Files
Chromacase/front/components/Hoverable.ts
Clément Le Bihan 495380ec43 Fix CI
2023-12-29 18:13:40 +01:00

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,
});
}