1 | import React, { Fragment } from 'react';
|
2 | import { TouchableOpacity, Modal, View, StatusBar, I18nManager, StyleSheet, Platform, } from 'react-native';
|
3 | import { withTheme } from '../config';
|
4 | import { ScreenWidth, ScreenHeight, isIOS } from '../helpers';
|
5 | import Triangle from './Triangle';
|
6 | import getTooltipCoordinate, { getElementVisibleWidth, } from './getTooltipCoordinate';
|
7 | const defaultProps = {
|
8 | withOverlay: true,
|
9 | overlayColor: 'rgba(250, 250, 250, 0.70)',
|
10 | highlightColor: 'transparent',
|
11 | withPointer: true,
|
12 | toggleOnPress: true,
|
13 | toggleAction: 'onPress',
|
14 | height: 40,
|
15 | width: 150,
|
16 | containerStyle: {},
|
17 | backgroundColor: '#617080',
|
18 | onClose: () => { },
|
19 | onOpen: () => { },
|
20 | skipAndroidStatusBar: false,
|
21 | ModalComponent: Modal,
|
22 | closeOnlyOnBackdropPress: false,
|
23 | };
|
24 | class Tooltip extends React.Component {
|
25 | constructor() {
|
26 | super(...arguments);
|
27 | this._isMounted = false;
|
28 | this.state = {
|
29 | isVisible: false,
|
30 | yOffset: 0,
|
31 | xOffset: 0,
|
32 | elementWidth: 0,
|
33 | elementHeight: 0,
|
34 | };
|
35 | this.toggleTooltip = () => {
|
36 | const { onClose } = this.props;
|
37 | this.getElementPosition();
|
38 | this._isMounted &&
|
39 | this.setState((prevState) => {
|
40 | if (prevState.isVisible) {
|
41 | onClose && onClose();
|
42 | }
|
43 | return { isVisible: !prevState.isVisible };
|
44 | });
|
45 | };
|
46 | this.wrapWithPress = (toggleOnPress, toggleAction, children) => {
|
47 | if (toggleOnPress) {
|
48 | return (<TouchableOpacity {...{ [toggleAction]: this.toggleTooltip }} delayLongPress={250} activeOpacity={1}>
|
49 | {children}
|
50 | </TouchableOpacity>);
|
51 | }
|
52 | return children;
|
53 | };
|
54 | this.containerStyle = (withOverlay, overlayColor) => {
|
55 | return {
|
56 | backgroundColor: withOverlay ? overlayColor : 'transparent',
|
57 | flex: 1,
|
58 | };
|
59 | };
|
60 | this.getTooltipStyle = () => {
|
61 | const { yOffset, xOffset, elementHeight, elementWidth } = this.state;
|
62 | const { height, backgroundColor, width, withPointer, containerStyle, } = this.props;
|
63 | const { x, y } = getTooltipCoordinate(xOffset, yOffset, elementWidth, elementHeight, ScreenWidth, ScreenHeight, width, height, withPointer);
|
64 | return StyleSheet.flatten([
|
65 | {
|
66 | position: 'absolute',
|
67 | [I18nManager.isRTL ? 'right' : 'left']: x,
|
68 | top: y,
|
69 | width,
|
70 | height,
|
71 | backgroundColor,
|
72 |
|
73 | display: 'flex',
|
74 | alignItems: 'center',
|
75 | justifyContent: 'center',
|
76 | flex: 1,
|
77 | borderRadius: 10,
|
78 | padding: 10,
|
79 | },
|
80 | containerStyle,
|
81 | ]);
|
82 | };
|
83 | this.renderPointer = (tooltipY) => {
|
84 | const { yOffset, xOffset, elementHeight, elementWidth } = this.state;
|
85 | const { backgroundColor, pointerColor } = this.props;
|
86 | const pastMiddleLine = yOffset > (tooltipY || 0);
|
87 | return (<View style={{
|
88 | position: 'absolute',
|
89 | top: pastMiddleLine ? yOffset - 13 : yOffset + elementHeight - 2,
|
90 | [I18nManager.isRTL ? 'right' : 'left']: xOffset +
|
91 | getElementVisibleWidth(elementWidth, xOffset, ScreenWidth) / 2 -
|
92 | 7.5,
|
93 | }}>
|
94 | <Triangle style={{ borderBottomColor: pointerColor || backgroundColor }} isDown={pastMiddleLine}/>
|
95 | </View>);
|
96 | };
|
97 | this.getTooltipHighlightedButtonStyle = () => {
|
98 | const { highlightColor } = this.props;
|
99 | const { yOffset, xOffset, elementWidth, elementHeight } = this.state;
|
100 | return {
|
101 | position: 'absolute',
|
102 | top: yOffset,
|
103 | [I18nManager.isRTL ? 'right' : 'left']: xOffset,
|
104 | backgroundColor: highlightColor,
|
105 | overflow: 'visible',
|
106 | width: elementWidth,
|
107 | height: elementHeight,
|
108 | };
|
109 | };
|
110 | this.renderTouchableHighlightedButton = () => {
|
111 | const TooltipHighlightedButtonStyle = this.getTooltipHighlightedButtonStyle();
|
112 | return (<TouchableOpacity testID="tooltipTouchableHighlightedButton" onPress={() => this.toggleTooltip()} style={TooltipHighlightedButtonStyle}>
|
113 | {this.props.children}
|
114 | </TouchableOpacity>);
|
115 | };
|
116 | this.renderStaticHighlightedButton = () => {
|
117 | const TooltipHighlightedButtonStyle = this.getTooltipHighlightedButtonStyle();
|
118 | return (<View style={TooltipHighlightedButtonStyle}>{this.props.children}</View>);
|
119 | };
|
120 | this.renderHighlightedButton = () => {
|
121 | const { closeOnlyOnBackdropPress } = this.props;
|
122 | if (closeOnlyOnBackdropPress) {
|
123 | return this.renderTouchableHighlightedButton();
|
124 | }
|
125 | else {
|
126 | return this.renderStaticHighlightedButton();
|
127 | }
|
128 | };
|
129 | this.renderContent = (withTooltip) => {
|
130 | const { popover, withPointer, toggleOnPress, toggleAction } = this.props;
|
131 | if (!withTooltip) {
|
132 | return this.wrapWithPress(toggleOnPress, toggleAction, this.props.children);
|
133 | }
|
134 | const tooltipStyle = this.getTooltipStyle();
|
135 | return (<View>
|
136 | {this.renderHighlightedButton()}
|
137 | {withPointer && this.renderPointer(tooltipStyle.top)}
|
138 | <View style={tooltipStyle} testID="tooltipPopoverContainer">
|
139 | {popover}
|
140 | </View>
|
141 | </View>);
|
142 | };
|
143 | this.getElementPosition = () => {
|
144 | const { skipAndroidStatusBar } = this.props;
|
145 | this.renderedElement &&
|
146 | this.renderedElement.measure((_frameOffsetX, _frameOffsetY, width, height, pageOffsetX, pageOffsetY) => {
|
147 | this._isMounted &&
|
148 | this.setState({
|
149 | xOffset: pageOffsetX,
|
150 | yOffset: isIOS || skipAndroidStatusBar
|
151 | ? pageOffsetY
|
152 | : pageOffsetY -
|
153 | Platform.select({
|
154 | android: StatusBar.currentHeight,
|
155 | ios: 20,
|
156 | default: 0,
|
157 | }),
|
158 | elementWidth: width,
|
159 | elementHeight: height,
|
160 | });
|
161 | });
|
162 | };
|
163 | this.renderStaticModalContent = () => {
|
164 | const { withOverlay, overlayColor } = this.props;
|
165 | return (<Fragment>
|
166 | <TouchableOpacity style={this.containerStyle(withOverlay, overlayColor)} onPress={this.toggleTooltip} activeOpacity={1}/>
|
167 | <View>{this.renderContent(true)}</View>
|
168 | </Fragment>);
|
169 | };
|
170 | this.renderTogglingModalContent = () => {
|
171 | const { withOverlay, overlayColor } = this.props;
|
172 | return (<TouchableOpacity style={this.containerStyle(withOverlay, overlayColor)} onPress={this.toggleTooltip} activeOpacity={1}>
|
173 | {this.renderContent(true)}
|
174 | </TouchableOpacity>);
|
175 | };
|
176 | this.renderModalContent = () => {
|
177 | const { closeOnlyOnBackdropPress } = this.props;
|
178 | if (closeOnlyOnBackdropPress) {
|
179 | return this.renderStaticModalContent();
|
180 | }
|
181 | else {
|
182 | return this.renderTogglingModalContent();
|
183 | }
|
184 | };
|
185 | }
|
186 | componentDidMount() {
|
187 | this._isMounted = true;
|
188 | // wait to compute onLayout values.
|
189 | requestAnimationFrame(this.getElementPosition);
|
190 | }
|
191 | componentWillUnmount() {
|
192 | this._isMounted = false;
|
193 | }
|
194 | render() {
|
195 | const { isVisible } = this.state;
|
196 | const { onOpen, ModalComponent } = this.props;
|
197 | return (<View collapsable={false} ref={(e) => {
|
198 | this.renderedElement = e;
|
199 | }}>
|
200 | {this.renderContent(false)}
|
201 | <ModalComponent animationType="fade" visible={isVisible} transparent onShow={onOpen}>
|
202 | {this.renderModalContent()}
|
203 | </ModalComponent>
|
204 | </View>);
|
205 | }
|
206 | }
|
207 | Tooltip.defaultProps = defaultProps;
|
208 | export { Tooltip };
|
209 | export default withTheme(Tooltip, 'Tooltip');
|