UNPKG

react-native-tips

Version:

Helper to show tips when you launch your react-native for the first time

569 lines (484 loc) 13 kB
import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import ReactMixin from 'react-mixin' import TimerMixin from 'react-timer-mixin' import styled from 'styled-components/native' import { Text, View, Modal, StyleSheet, TouchableOpacity, ViewPropTypes } from 'react-native' const getArrowStyleByPosition = (position = 'top') => { switch (position) { case 'right': return { left: -5, top: '50%', marginTop: 5, borderTopWidth: 5, borderRightWidth: 5, borderBottomWidth: 5, borderTopColor: 'transparent', borderBottomColor: 'transparent', borderRightColor: 'rgba(0, 0, 0, 0.5)' } case 'left': return { right: -5, top: '50%', marginTop: 5, borderTopWidth: 5, borderLeftWidth: 5, borderBottomWidth: 5, borderTopColor: 'transparent', borderBottomColor: 'transparent', borderLeftColor: 'rgba(0, 0, 0, 0.5)' } case 'bottom': return { top: -5, left: '50%', marginLeft: 2.5, borderLeftWidth: 5, borderRightWidth: 5, borderBottomWidth: 5, borderLeftColor: 'transparent', borderRightColor: 'transparent', borderBottomColor: 'rgba(0, 0, 0, 0.5)' } default: return { bottom: -5, left: '50%', marginLeft: 2.5, borderTopWidth: 5, borderLeftWidth: 5, borderRightWidth: 5, borderLeftColor: 'transparent', borderRightColor: 'transparent', borderTopColor: 'rgba(0, 0, 0, 0.5)' } } } const TooltipArrow = styled.View` position: absolute; shadow-radius: 1px; shadow-color: black; shadow-opacity: 0.5; ` const Tooltip = styled.View` background: rgba(0, 0, 0, 0.5); flex: -1; padding: 10px; border-radius: 4px; shadow-radius: 1px; shadow-color: black; shadow-opacity: 0.5; ` const ModalContent = styled.View` width: 80%; display: flex; align-self: center; position: absolute; flex-wrap: nowrap; ` const ChildrenOverlay = styled.TouchableOpacity` position: absolute; z-index: 1; top: 0; left: 0; right: 0; bottom: 0; ` const HighlightView = styled.View` position: relative; z-index: 0; ` const styles = StyleSheet.create({ tooltipContainer: { position: 'relative', flex: 1 }, modal: { backgroundColor: 'rgba(0, 0, 0, 0.6)', position: 'relative', flex: 1 }, text: { color: 'white', textAlign: 'center' } }) export default class Tips extends PureComponent { /* LIFECYCLE */ /** * @constructor * @param {*} props - Properties of the PureComponent */ constructor(props) { super(props) this.state = { /** * The position of the children in x axis * @type {Number} */ componentLeft: 0, /** * The position of the children in y axis * @type {Number} */ componentTop: 0, /** * The width of the children * @type {Number} */ componentWidth: 0, /** * The height of the children * @type {Number} */ componentHeight: 0, /** * The position of the tooltip in x axis * @type {Number} */ tooltipLeft: 0, /** * The position of the tooltip in y axis * @type {Number} */ tooltipTop: 0, /** * Check if the component is ready * @type {Boolean} */ ready: false } this.view = null this.timeoutDelay = null this.handleLayout = this.handleLayout.bind(this) this.handleTooltipLayout = this.handleTooltipLayout.bind(this) this.handleRequestClose = this.handleRequestClose.bind(this) } /** * @componentDidMount */ componentDidMount() { if (this.props.visible) { this.updateComponentPosition(true) } } /** * @componentWillReceiveProps * @param {*} nextProps - Next properties of the PureComponent */ componentWillReceiveProps(nextProps, nextState) { if (nextProps.visible && !this.props.visible && !nextState.ready) { this.updateComponentPosition(true) } else if (!nextProps.visible && this.props.visible && nextState.ready) { this.setState({ ready: false }) } } /* METHODS */ /** * Extends the stylesheet of the tooltip container to be relative to the * position passed in parameters * @param {String} position - The position * @returns {Object} Stylesheet properties */ getModalContentStyleByPosition(position) { switch (position) { case 'right': case 'left': return { flexDirection: 'row' } case 'top': case 'bottom': return { flexDirection: 'column' } default: return {} } } /** * Call the measure to ensure positionning * @param {Boolean=} willBeReady - If true, the component state will be ready to be visible */ updateComponentPosition(willBeReady = false) { const { delay } = this.props this.requestAnimationFrame(() => { this.view.measure((x, y, width, height, pageX, pageY) => { this.setState(state => ({ componentLeft: pageX, componentTop: pageY, componentWidth: state.componentWidth || width, componentHeight: state.componentHeight || height }), () => { if (willBeReady) { clearTimeout(this.timeoutDelay) this.timeoutDelay = this.setTimeout(() => this.setState({ ready: true }), delay) } }) }) }) } /** * Measure the size of the children * @param {*} event - The event object */ handleLayout(event) { const { width, height } = event.nativeEvent.layout this.setState({ componentWidth: width, componentHeight: height }) this.updateComponentPosition() } /** * Measure the size of the tooltip layout * @param {*} event - The event object */ handleTooltipLayout(event) { const { position } = this.props const { width, height } = event.nativeEvent.layout this.setState((state) => { const { componentLeft, componentTop, componentWidth, componentHeight } = state const nextState = {} switch (position) { case 'right': nextState.tooltipTop = Math.max(-componentTop, (componentHeight / 2) - (height / 2)) nextState.tooltipLeft = 10 break case 'left': nextState.tooltipTop = Math.max(-componentTop, (componentHeight / 2) - (height / 2)) nextState.tooltipLeft = -componentLeft - width - 10 break case 'bottom': nextState.tooltipLeft = Math.max(-componentLeft, (componentWidth / 2) - (width / 2)) nextState.tooltipTop = 10 break case 'top': nextState.tooltipTop = Math.max(-componentTop, -componentHeight - height - 10) nextState.tooltipLeft = Math.max(-componentLeft, (componentWidth / 2) - (width / 2)) break default: nextState.tooltipTop = 0 nextState.tooltipLeft = 0 break } return nextState }) } /** * Handle event to switch between 'onRequestNext' and 'onRequestClose' */ handleRequestClose() { const { onRequestNext, onRequestClose } = this.props if (onRequestNext) { return onRequestNext() } return !!onRequestClose && onRequestClose() } /* RENDER */ /** * @render * @returns {JSX} */ render() { const { children, position, text, childrenStyle, tooltipArrowStyle, modalStyle, textStyle, style, contentStyle, enableChildrenInteraction, tooltipContainerStyle, content, offsetLeft, offsetTop } = this.props const { componentLeft, componentTop, componentWidth: width, componentHeight: height, tooltipLeft, tooltipTop, ready } = this.state const left = componentLeft + offsetLeft const top = componentTop + offsetTop const visible = this.props.visible && ready return ( <View collapsable={false} renderToHardwareTextureAndroid ref={(view) => { this.view = view }} > <View onLayout={this.handleLayout}>{children}</View> <Modal animationType="fade" visible={visible} transparent onRequestClose={this.handleRequestClose} > <TouchableOpacity activeOpacity={1} focusedOpacity={1} style={{ flex: 1 }} onPress={this.handleRequestClose} > <View style={[styles.modal, modalStyle]}> <ModalContent style={[ this.getModalContentStyleByPosition(position), { left, top }, contentStyle ]} > <ChildrenOverlay onPress={this.handleRequestClose} style={styles.childrenOverlay} /> <HighlightView style={[{ width, height, zIndex: enableChildrenInteraction ? 2 : 0 }, childrenStyle ]} > {children} </HighlightView> <View onLayout={this.handleTooltipLayout} style={[styles.tooltipContainer, { top: tooltipTop, left: tooltipLeft }, tooltipContainerStyle]} > <Tooltip style={style} > {content} {!!text && <Text style={[styles.text, textStyle]}>{text}</Text>} {position !== 'none' && ( <TooltipArrow style={[getArrowStyleByPosition(position), tooltipArrowStyle]} /> )} </Tooltip> </View> </ModalContent> </View> </TouchableOpacity> </Modal> </View> ) } } Tips.defaultProps = { visible: false, style: {}, textStyle: {}, modalStyle: {}, contentStyle: {}, childrenStyle: {}, tooltipArrowStyle: {}, tooltipContainerStyle: {}, children: [], content: [], offsetLeft: 0, offsetTop: 0, delay: 250, text: '', position: 'top', onRequestClose: () => {}, onRequestNext: null, enableChildrenInteraction: false } Tips.propTypes = { /** * Define the position of your tips related to the children * @type {String} */ position: PropTypes.oneOf(['left', 'top', 'bottom', 'right', 'none']), /** * Override the style of your tips * @type {Stylesheet} */ style: ViewPropTypes.style, /** * Override the style of the container of your tips (used for positionning) * @type {Stylesheet} */ tooltipContainerStyle: ViewPropTypes.style, /** * Add style to the tooltip arrow * @type {Stylesheet} */ tooltipArrowStyle: ViewPropTypes.style, /** * Override the style of the Modal Component (react-native) * @type {Stylesheet} */ modalStyle: ViewPropTypes.style, /** * Override the style of the content of the Modal (used for positionning * the highlight elements and tips) * @type {Stylesheet} */ contentStyle: ViewPropTypes.style, /** * Override the style of the container of the children * @type {Stylesheet} */ childrenStyle: ViewPropTypes.style, /** * Override the style of the text inside the Tips * @type {Stylesheet} */ textStyle: Text.propTypes.style, /** * Add an offset of the Tips in x axis. * @type {Number} */ offsetLeft: PropTypes.number, /** * Add an offset of the Tips in y axis. * @type {Number} */ offsetTop: PropTypes.number, /** * Triggered when the user tap on the screen. * @type {Function} */ onRequestClose: PropTypes.func, /** * @deprecated * Deprecated !!! Use onRequestClose instead. (See #waterfall-tips for more.) * @type {Function} */ onRequestNext: PropTypes.func, /** * The `children` of Tips are elements that will be highlighted when the tips will be visible * @type {Node} */ children: PropTypes.node, /** * Text inside the Tips. * @type {String} */ text: PropTypes.string, /** * Use this property if you want to add more than a simple text inside your Tips. * @type {Node} */ content: PropTypes.node, /** * Add a delay before showing the Tips * @default 250 * @type {Number} */ delay: PropTypes.number, /** * Set the visibility of your Tips * @default false * @type {Boolean} */ visible: PropTypes.bool, /** * If set to true, interation with children won't close the Tips * @default false * @type {Boolean} */ enableChildrenInteraction: PropTypes.bool } ReactMixin(Tips.prototype, TimerMixin)