136 lines
3.7 KiB
TypeScript
136 lines
3.7 KiB
TypeScript
// 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 } 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,
|
|
};
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return React.cloneElement(React.Children.only(children) as any, {
|
|
...webProps,
|
|
// if child is Touchable
|
|
onPressIn: handleGrant,
|
|
onPressOut: handleRelease,
|
|
});
|
|
}
|