Files
2023-01-20 18:34:50 +08:00

357 lines
8.8 KiB
TypeScript

import React, { Fragment } from 'react';
import {
TouchableOpacity,
Modal,
View,
StatusBar,
I18nManager,
ViewStyle,
FlexStyle,
StyleProp,
StyleSheet,
ColorValue,
Platform,
Keyboard,
} from 'react-native';
import { withTheme } from 'react-native-elements/dist/config';
import { ThemeProps } from 'react-native-elements/dist/config';
import {
ScreenWidth,
ScreenHeight,
isIOS,
} from 'react-native-elements/dist/helpers';
import Triangle from './Triangle';
import getTooltipCoordinate, {
getElementVisibleWidth,
} from './getTooltipCoordinate';
export type TooltipProps = {
withPointer?: boolean;
popover?: React.ReactElement<{}>;
toggleOnPress?: boolean;
toggleAction?: string | 'onPress' | 'onLongPress';
height?: FlexStyle['height'];
width?: FlexStyle['width'];
containerStyle?: StyleProp<ViewStyle>;
pointerColor?: ColorValue;
onClose?(): void;
onOpen?(): void;
overlayColor?: ColorValue;
withOverlay?: boolean;
backgroundColor?: ColorValue;
highlightColor?: ColorValue;
skipAndroidStatusBar?: boolean;
ModalComponent?: typeof React.Component;
closeOnlyOnBackdropPress?: boolean;
} & typeof defaultProps;
const defaultProps = {
withOverlay: true,
overlayColor: 'rgba(250, 250, 250, 0.70)',
highlightColor: 'transparent',
withPointer: true,
toggleOnPress: true,
toggleAction: 'onPress',
height: 40,
width: 150,
containerStyle: {},
backgroundColor: '#617080',
onClose: () => {},
onOpen: () => {},
skipAndroidStatusBar: false,
ModalComponent: Modal,
closeOnlyOnBackdropPress: false,
};
type TooltipState = {
isVisible: boolean;
yOffset: number;
xOffset: number;
elementWidth: number;
elementHeight: number;
};
class Tooltip extends React.Component<
TooltipProps & Partial<ThemeProps<TooltipProps>>,
TooltipState
> {
static defaultProps = defaultProps;
_isMounted: boolean = false;
state = {
isVisible: false,
yOffset: 0,
xOffset: 0,
elementWidth: 0,
elementHeight: 0,
};
renderedElement?: View | null;
toggleTooltip = () => {
const { onClose } = this.props;
Keyboard.dismiss();
setTimeout(() => {
this.getElementPosition();
this._isMounted &&
this.setState((prevState) => {
if (prevState.isVisible) {
onClose && onClose();
}
return { isVisible: !prevState.isVisible };
});
}, 100);
};
wrapWithPress = (
toggleOnPress: TooltipProps['toggleOnPress'],
toggleAction: TooltipProps['toggleAction'],
children: React.ReactNode
) => {
if (toggleOnPress) {
return (
<TouchableOpacity
{...{ [toggleAction]: this.toggleTooltip }}
delayLongPress={250}
activeOpacity={1}>
{children}
</TouchableOpacity>
);
}
return children;
};
containerStyle = (withOverlay: boolean, overlayColor: string): ViewStyle => {
return {
backgroundColor: withOverlay ? overlayColor : 'transparent',
flex: 1,
};
};
getTooltipStyle = () => {
const { yOffset, xOffset, elementHeight, elementWidth } = this.state;
const { height, backgroundColor, width, withPointer, containerStyle } =
this.props;
const { x, y } = getTooltipCoordinate(
xOffset,
yOffset,
elementWidth,
elementHeight,
ScreenWidth,
ScreenHeight,
width,
height,
withPointer
);
return StyleSheet.flatten([
{
position: 'absolute',
[I18nManager.isRTL ? 'right' : 'left']: x,
top: y,
width,
height,
backgroundColor,
// default styles
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: 1,
borderRadius: 10,
padding: 10,
},
containerStyle,
]);
};
renderPointer = (tooltipY: FlexStyle['top']) => {
const { yOffset, xOffset, elementHeight, elementWidth } = this.state;
const { backgroundColor, pointerColor } = this.props;
const pastMiddleLine = yOffset > (tooltipY || 0);
return (
<View
style={{
position: 'absolute',
top: pastMiddleLine ? yOffset - 13 : yOffset + elementHeight - 2,
[I18nManager.isRTL ? 'right' : 'left']:
xOffset +
getElementVisibleWidth(elementWidth, xOffset, ScreenWidth) / 2 -
7.5,
}}>
<Triangle
style={{ borderBottomColor: pointerColor || backgroundColor }}
isDown={pastMiddleLine}
/>
</View>
);
};
getTooltipHighlightedButtonStyle = (): ViewStyle => {
const { highlightColor } = this.props;
const { yOffset, xOffset, elementWidth, elementHeight } = this.state;
return {
position: 'absolute',
top: yOffset,
[I18nManager.isRTL ? 'right' : 'left']: xOffset,
backgroundColor: highlightColor,
overflow: 'visible',
width: elementWidth,
height: elementHeight,
};
};
renderTouchableHighlightedButton = () => {
const TooltipHighlightedButtonStyle =
this.getTooltipHighlightedButtonStyle();
return (
<TouchableOpacity
testID="tooltipTouchableHighlightedButton"
onPress={() => this.toggleTooltip()}
style={TooltipHighlightedButtonStyle}>
{this.props.children}
</TouchableOpacity>
);
};
renderStaticHighlightedButton = () => {
const TooltipHighlightedButtonStyle =
this.getTooltipHighlightedButtonStyle();
return (
<View style={TooltipHighlightedButtonStyle}>{this.props.children}</View>
);
};
renderHighlightedButton = () => {
const { closeOnlyOnBackdropPress } = this.props;
if (closeOnlyOnBackdropPress) {
return this.renderTouchableHighlightedButton();
} else {
return this.renderStaticHighlightedButton();
}
};
renderContent = (withTooltip: boolean) => {
const { popover, withPointer, toggleOnPress, toggleAction } = this.props;
if (!withTooltip) {
return this.wrapWithPress(
toggleOnPress,
toggleAction,
this.props.children
);
}
const tooltipStyle = this.getTooltipStyle() as ViewStyle;
return (
<View>
{this.renderHighlightedButton()}
{withPointer && this.renderPointer(tooltipStyle.top)}
<View style={tooltipStyle} testID="tooltipPopoverContainer">
{popover}
</View>
</View>
);
};
componentDidMount() {
this._isMounted = true;
// wait to compute onLayout values.
requestAnimationFrame(this.getElementPosition);
}
componentWillUnmount() {
this._isMounted = false;
}
getElementPosition = () => {
const { skipAndroidStatusBar } = this.props;
this.renderedElement &&
this.renderedElement.measure(
(
_frameOffsetX,
_frameOffsetY,
width,
height,
pageOffsetX,
pageOffsetY
) => {
this._isMounted &&
this.setState({
xOffset: pageOffsetX,
yOffset:
isIOS || skipAndroidStatusBar
? pageOffsetY
: pageOffsetY -
Platform.select({
android: StatusBar.currentHeight,
ios: 20,
default: 0,
}),
elementWidth: width,
elementHeight: height,
});
}
);
};
renderStaticModalContent = () => {
const { withOverlay, overlayColor } = this.props;
return (
<Fragment>
<TouchableOpacity
style={this.containerStyle(withOverlay, overlayColor)}
onPress={this.toggleTooltip}
activeOpacity={1}
/>
<View>{this.renderContent(true)}</View>
</Fragment>
);
};
renderTogglingModalContent = () => {
const { withOverlay, overlayColor } = this.props;
return (
<TouchableOpacity
style={this.containerStyle(withOverlay, overlayColor)}
onPress={this.toggleTooltip}
activeOpacity={1}>
{this.renderContent(true)}
</TouchableOpacity>
);
};
renderModalContent = () => {
const { closeOnlyOnBackdropPress } = this.props;
if (closeOnlyOnBackdropPress) {
return this.renderStaticModalContent();
} else {
return this.renderTogglingModalContent();
}
};
render() {
const { isVisible } = this.state;
const { onOpen, ModalComponent } = this.props;
return (
<View
collapsable={false}
ref={(e) => {
this.renderedElement = e;
}}>
{this.renderContent(false)}
<ModalComponent
animationType="fade"
visible={isVisible}
transparent
onShow={onOpen}>
{this.renderModalContent()}
</ModalComponent>
</View>
);
}
}
export { Tooltip };
export default withTheme(Tooltip, 'Tooltip');