-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
Copy pathBaseEducationalTooltip.tsx
145 lines (127 loc) · 5.77 KB
/
BaseEducationalTooltip.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import {NavigationContext} from '@react-navigation/native';
import React, {memo, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState} from 'react';
import type {LayoutRectangle, NativeMethods, NativeSyntheticEvent} from 'react-native';
import {DeviceEventEmitter, Dimensions} from 'react-native';
import GenericTooltip from '@components/Tooltip/GenericTooltip';
import type {EducationalTooltipProps, GenericTooltipState} from '@components/Tooltip/types';
import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import measureTooltipCoordinate, {getTooltipCoordiate} from './measureTooltipCoordinate';
type LayoutChangeEventWithTarget = NativeSyntheticEvent<{layout: LayoutRectangle; target: HTMLElement}>;
type ScrollingEventData = {
isScrolling: boolean;
};
/**
* A component used to wrap an element intended for displaying a tooltip.
* This tooltip would show immediately without user's interaction and hide after 5 seconds.
*/
function BaseEducationalTooltip({children, shouldRender = false, shouldHideOnNavigate = true, shouldHideOnScroll = false, ...props}: EducationalTooltipProps) {
const genericTooltipStateRef = useRef<GenericTooltipState>();
const tooltipElRef = useRef<React.Component & Readonly<NativeMethods>>();
const [shouldMeasure, setShouldMeasure] = useState(false);
const show = useRef<() => void>();
const navigator = useContext(NavigationContext);
const insets = useSafeAreaInsets();
const setTooltipPosition = useCallback(
(isScrolling: boolean) => {
if (!shouldHideOnScroll || !genericTooltipStateRef.current || !tooltipElRef.current) {
return;
}
const {hideTooltip, showTooltip, updateTargetBounds} = genericTooltipStateRef.current;
if (isScrolling) {
hideTooltip();
} else {
getTooltipCoordiate(tooltipElRef.current, (bounds) => {
updateTargetBounds(bounds);
const {y, height} = bounds;
const offset = 30; // Buffer space
const dimensions = Dimensions.get('window');
const top = y - (insets.top || 0);
const bottom = y + height + insets.bottom || 0;
// Calculate the available space at the top, considering the header height and offset
const availableHeightForTop = top - (variables.contentHeaderHeight - offset);
// Calculate the total height available after accounting for the bottom tab and offset
const availableHeightForBottom = dimensions.height - (bottom + variables.bottomTabHeight - offset);
if (availableHeightForTop < 0 || availableHeightForBottom < 0) {
hideTooltip();
} else {
showTooltip();
}
});
}
},
[insets, shouldHideOnScroll],
);
useLayoutEffect(() => {
if (!shouldRender || !shouldHideOnScroll) {
return;
}
setTooltipPosition(false);
const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, ({isScrolling}: ScrollingEventData = {isScrolling: false}) => {
setTooltipPosition(isScrolling);
});
return () => scrollingListener.remove();
}, [shouldRender, shouldHideOnScroll, setTooltipPosition]);
useEffect(() => {
return () => {
genericTooltipStateRef.current?.hideTooltip();
};
}, []);
useEffect(() => {
if (!shouldMeasure) {
return;
}
if (!shouldRender) {
genericTooltipStateRef.current?.hideTooltip();
return;
}
// When tooltip is used inside an animated view (e.g. popover), we need to wait for the animation to finish before measuring content.
const timerID = setTimeout(() => {
show.current?.();
}, 500);
return () => {
clearTimeout(timerID);
};
}, [shouldMeasure, shouldRender]);
useEffect(() => {
if (!navigator) {
return;
}
const unsubscribe = navigator.addListener('blur', () => {
if (!shouldHideOnNavigate) {
return;
}
genericTooltipStateRef.current?.hideTooltip();
});
return unsubscribe;
}, [navigator, shouldHideOnNavigate]);
return (
<GenericTooltip
shouldForceAnimate
shouldRender={shouldRender}
isEducationTooltip
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{(genericTooltipState) => {
const {updateTargetBounds, showTooltip} = genericTooltipState;
// eslint-disable-next-line react-compiler/react-compiler
genericTooltipStateRef.current = genericTooltipState;
return React.cloneElement(children as React.ReactElement, {
onLayout: (e: LayoutChangeEventWithTarget) => {
if (!shouldMeasure) {
setShouldMeasure(true);
}
// e.target is specific to native, use e.nativeEvent.target on web instead
const target = e.target || e.nativeEvent.target;
tooltipElRef.current = target;
show.current = () => measureTooltipCoordinate(target, updateTargetBounds, showTooltip);
},
});
}}
</GenericTooltip>
);
}
BaseEducationalTooltip.displayName = 'BaseEducationalTooltip';
export default memo(BaseEducationalTooltip);