import * as React from 'react'; import { Dimensions, View, LayoutChangeEvent, StyleSheet, Platform, Pressable, ViewStyle, } from 'react-native'; import type { ThemeProp } from 'src/types'; import { getTooltipPosition, Measurement } from './utils'; import { useInternalTheme } from '../../core/theming'; import { addEventListener } from '../../utils/addEventListener'; import Portal from '../Portal/Portal'; import Text from '../Typography/Text'; export type Props = { /** * Tooltip reference element. Needs to be able to hold a ref. */ children: React.ReactElement; /** * The number of milliseconds a user must touch the element before showing the tooltip. */ enterTouchDelay?: number; /** * The number of milliseconds after the user stops touching an element before hiding the tooltip. */ leaveTouchDelay?: number; /** * Tooltip title */ title: string; /** * Specifies the largest possible scale a title font can reach. */ titleMaxFontSizeMultiplier?: number; /** * @optional */ theme?: ThemeProp; }; /** * Tooltips display informative text when users hover over, focus on, or tap an element. * * Plain tooltips, when activated, display a text label identifying an element, such as a description of its function. Tooltips should include only short, descriptive text and avoid restating visible UI text. * * ## Usage * ```js * import * as React from 'react'; * import { IconButton, Tooltip } from 'react-native-paper'; * * const MyComponent = () => ( * * {}} /> * * ); * * export default MyComponent; * ``` */ const Tooltip = ({ children, enterTouchDelay = 500, leaveTouchDelay = 1500, title, theme: themeOverrides, titleMaxFontSizeMultiplier, ...rest }: Props) => { const isWeb = Platform.OS === 'web'; const theme = useInternalTheme(themeOverrides); const [visible, setVisible] = React.useState(false); const [measurement, setMeasurement] = React.useState({ children: {}, tooltip: {}, measured: false, }); const showTooltipTimer = React.useRef([]); const hideTooltipTimer = React.useRef([]); const childrenWrapperRef = React.useRef() as React.MutableRefObject; const touched = React.useRef(false); React.useEffect(() => { return () => { if (showTooltipTimer.current.length) { showTooltipTimer.current.forEach((t) => clearTimeout(t)); showTooltipTimer.current = []; } if (hideTooltipTimer.current.length) { hideTooltipTimer.current.forEach((t) => clearTimeout(t)); hideTooltipTimer.current = []; } }; }, []); React.useEffect(() => { const subscription = addEventListener(Dimensions, 'change', () => setVisible(false) ); return () => subscription.remove(); }, []); const handleOnLayout = ({ nativeEvent: { layout } }: LayoutChangeEvent) => { childrenWrapperRef.current.measure( (_x, _y, width, height, pageX, pageY) => { setMeasurement({ children: { pageX, pageY, height, width }, tooltip: { ...layout }, measured: true, }); } ); }; const handleTouchStart = () => { if (hideTooltipTimer.current.length) { hideTooltipTimer.current.forEach((t) => clearTimeout(t)); hideTooltipTimer.current = []; } if (isWeb) { let id = setTimeout(() => { touched.current = true; setVisible(true); }, enterTouchDelay) as unknown as NodeJS.Timeout; showTooltipTimer.current.push(id); } else { touched.current = true; setVisible(true); } }; const handleTouchEnd = () => { touched.current = false; if (showTooltipTimer.current.length) { showTooltipTimer.current.forEach((t) => clearTimeout(t)); showTooltipTimer.current = []; } let id = setTimeout(() => { setVisible(false); setMeasurement({ children: {}, tooltip: {}, measured: false }); }, leaveTouchDelay) as unknown as NodeJS.Timeout; hideTooltipTimer.current.push(id); }; const mobilePressProps = { onPress: React.useCallback(() => { if (touched.current) { return null; } else { if (children.props.disabled) return null; return children.props.onPress?.(); } }, [children.props]), onLongPress: () => handleTouchStart(), onPressOut: () => handleTouchEnd(), delayLongPress: enterTouchDelay, }; const webPressProps = { onHoverIn: () => { handleTouchStart(); children.props.onHoverIn?.(); }, onHoverOut: () => { handleTouchEnd(); children.props.onHoverOut?.(); }, }; return ( <> {visible && ( {title} )} {/* Need the xxPressProps in both places */} {React.cloneElement(children, { ...rest, ...(isWeb ? webPressProps : mobilePressProps), })} ); }; Tooltip.displayName = 'Tooltip'; const styles = StyleSheet.create({ tooltip: { alignSelf: 'flex-start', justifyContent: 'center', paddingHorizontal: 16, height: 32, maxHeight: 32, }, visible: { opacity: 1, }, hidden: { opacity: 0, }, pressContainer: { ...(Platform.OS === 'web' && { cursor: 'default' }), } as ViewStyle, }); export default Tooltip;