Last active
February 15, 2023 12:37
-
-
Save nandorojo/066ff2f40419b7e06054cc7282e24f8d to your computer and use it in GitHub Desktop.
React Native Web Hoverability (with react-native-reanimated)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// credit to https://gist.github.com/ianmartorell/32bb7df95e5eff0a5ee2b2f55095e6a6 | |
// this file was repurosed from there | |
// via this issue https://gist.github.com/necolas/1c494e44e23eb7f8c5864a2fac66299a | |
// because RNW's pressable doesn't bubble events to parent pressables: https://github.com/necolas/react-native-web/issues/1875 | |
/* eslint-disable no-inner-declarations */ | |
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment' | |
let isEnabled = false | |
if (canUseDOM) { | |
/** | |
* Web browsers emulate mouse events (and hover states) after touch events. | |
* This code infers when the currently-in-use modality supports hover | |
* (including for multi-modality devices) and considers "hover" to be enabled | |
* if a mouse movement occurs more than 1 second after the last touch event. | |
* This threshold is long enough to account for longer delays between the | |
* browser firing touch and mouse events on low-powered devices. | |
*/ | |
const HOVER_THRESHOLD_MS = 1000 | |
let lastTouchTimestamp = 0 | |
function enableHover() { | |
if (isEnabled || Date.now() - lastTouchTimestamp < HOVER_THRESHOLD_MS) { | |
return | |
} | |
isEnabled = true | |
} | |
function disableHover() { | |
lastTouchTimestamp = Date.now() | |
if (isEnabled) { | |
isEnabled = false | |
} | |
} | |
document.addEventListener('touchstart', disableHover, true) | |
document.addEventListener('touchmove', disableHover, true) | |
document.addEventListener('mousemove', enableHover, true) | |
} | |
function isHoverEnabled(): boolean { | |
return isEnabled | |
} | |
import React, { useCallback, ReactChild, useRef } from 'react' | |
import { | |
useSharedValue, | |
useAnimatedReaction, | |
runOnJS, | |
} from 'react-native-reanimated' | |
import { Platform } from 'react-native' | |
export interface HoverableProps { | |
onHoverIn?: () => void | |
onHoverOut?: () => void | |
onPressIn?: () => void | |
onPressOut?: () => void | |
children: NonNullable<ReactChild> | |
} | |
export default function Hoverable({ | |
onHoverIn, | |
onHoverOut, | |
children, | |
onPressIn, | |
onPressOut, | |
}: HoverableProps) { | |
const showHover = useSharedValue(true) | |
const isHovered = useSharedValue(false) | |
const hoverIn = useRef<undefined | (() => void)>(() => onHoverIn?.()) | |
const hoverOut = useRef<undefined | (() => void)>(() => onHoverOut?.()) | |
const pressIn = useRef<undefined | (() => void)>(() => onPressIn?.()) | |
const pressOut = useRef<undefined | (() => void)>(() => onPressOut?.()) | |
hoverIn.current = onHoverIn | |
hoverOut.current = onHoverOut | |
pressIn.current = onPressIn | |
pressOut.current = onPressOut | |
useAnimatedReaction( | |
() => { | |
return Platform.OS === 'web' && showHover.value && isHovered.value | |
}, | |
(hovered, previouslyHovered) => { | |
if (hovered !== previouslyHovered) { | |
if (hovered && hoverIn.current) { | |
// no need for runOnJS, it's always web | |
hoverIn.current() | |
} else if (hoverOut.current) { | |
hoverOut.current() | |
} | |
} | |
}, | |
[] | |
) | |
const handleMouseEnter = useCallback(() => { | |
if (isHoverEnabled() && !isHovered.value) { | |
isHovered.value = true | |
} | |
}, [isHovered]) | |
const handleMouseLeave = useCallback(() => { | |
if (isHovered.value) { | |
isHovered.value = false | |
} | |
}, [isHovered]) | |
const handleGrant = useCallback(() => { | |
showHover.value = false | |
pressIn.current?.() | |
}, [showHover]) | |
const handleRelease = useCallback(() => { | |
showHover.value = true | |
pressOut.current?.() | |
}, [showHover]) | |
let webProps = {} | |
if (Platform.OS === 'web') { | |
webProps = { | |
onMouseEnter: handleMouseEnter, | |
onMouseLeave: handleMouseLeave, | |
// prevent hover showing while responder | |
onResponderGrant: handleGrant, | |
onResponderRelease: handleRelease, | |
} | |
} | |
return React.cloneElement(React.Children.only(children) as any, { | |
...webProps, | |
// if child is Touchable | |
onPressIn: handleGrant, | |
onPressOut: handleRelease, | |
}) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Hoverability onHoverIn={onHoverIn} onHoverOut={onHoverOut}> | |
<Pressable> | |
</Pressable> | |
</Hoverability> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment