react-native-walkthrough-tooltip
Advanced tools
Comparing version
{ | ||
"name": "react-native-walkthrough-tooltip", | ||
"version": "0.6.0-alpha.4", | ||
"version": "0.6.0", | ||
"description": "An inline wrapper for calling out React Native components via tooltip", | ||
@@ -27,4 +27,3 @@ "main": "src/tooltip.js", | ||
"dependencies": { | ||
"prop-types": "^15.6.1", | ||
"react-fast-compare": "^2.0.4" | ||
"prop-types": "^15.6.1" | ||
}, | ||
@@ -50,2 +49,5 @@ "devDependencies": { | ||
}, | ||
"peerDependencies": { | ||
"@types/react": "^16.8.24" | ||
}, | ||
"jest": { | ||
@@ -52,0 +54,0 @@ "preset": "react-native", |
@@ -51,3 +51,2 @@ # React Native Walkthrough Tooltip [](https://www.npmjs.com/package/react-native-walkthrough-tooltip) [](https://www.npmjs.com/package/react-native-walkthrough-tooltip) | ||
| content | function/Element | `<View />` | This is the view displayed in the tooltip popover bubble | | ||
| contentBoundByDisplayArea | bool | true | When true, the tooltip content will respect the displayArea, preventing tooltip from rendering outside of the displayArea (helpful to prevent tooltips rendering partially off screen) | | ||
| displayArea | `Rect` | fullscreen `Rect` with 24px of padding | Screen area where the tooltip may be displayed | | ||
@@ -92,1 +91,20 @@ | isVisible | bool | false | When true, tooltip is displayed | | ||
One possible use case for these functions would be a scenerio where you are highlighting new functionality and want to restrict a user to ONLY do a certain action when they press on an element. While perhaps uncommon, this use case was relevant for another library I am working on, so it may be useful for you. When these props are NOT provided, all touch events on children occur as expected. | ||
### TooltipChildrenConsumer | ||
[React Context](https://reactjs.org/docs/context.html) consumer that can be used to distinguish "real" children rendered inside parent's layout from their copies rendered inside tooltip's modal. The duplicate child rendered in the tooltip modal is wrapped in a Context.Provider which provides object with prop `tooltipDuplicate` set to `true`, so informed decisions may be made, if necessary, based on where the child rendered. | ||
```js | ||
import Tooltip, { TooltipChildrenConsumer } from 'react-native-walkthrough-tooltip'; | ||
... | ||
<Tooltip withContext> | ||
<ComponentA /> | ||
<ComponentB> | ||
<TooltipChildrenConsumer> | ||
{({ tooltipDuplicate }) => ( | ||
// will only assign a ref to the original component | ||
<FlatList {...(!tooltipDuplicate && { ref: this.listRef })} /> | ||
)} | ||
</WalkthroughConsumer> | ||
</ComponentB> | ||
</Tooltip> | ||
``` |
163
src/geom.js
@@ -24,46 +24,23 @@ class Point { | ||
const getBoundsForDisplayArea = displayArea => ({ | ||
x: { | ||
min: displayArea.x, | ||
max: displayArea.x + displayArea.width, | ||
}, | ||
y: { | ||
min: displayArea.y, | ||
max: displayArea.y + displayArea.height, | ||
}, | ||
}); | ||
const computeTopGeometry = ({ displayArea, childRect, contentSize, arrowSize }) => { | ||
const computeTopGeometry = ({ | ||
displayArea, | ||
childRect, | ||
contentSize, | ||
arrowSize, | ||
}) => { | ||
const tooltipOrigin = new Point( | ||
Math.min( | ||
displayArea.x + displayArea.width - contentSize.width, | ||
Math.max(displayArea.x, childRect.x + (childRect.width - contentSize.width) / 2), | ||
Math.max( | ||
displayArea.x, | ||
childRect.x + (childRect.width - contentSize.width) / 2, | ||
), | ||
), | ||
childRect.y - contentSize.height - arrowSize.height, | ||
); | ||
const anchorPoint = new Point(childRect.x + childRect.width / 2.0, childRect.y); | ||
const anchorPoint = new Point( | ||
childRect.x + childRect.width / 2.0, | ||
childRect.y, | ||
); | ||
// compute bound content size | ||
const boundTooltipOrigin = new Point(tooltipOrigin.x, tooltipOrigin.y); | ||
const boundContentSize = new Size(contentSize.width, contentSize.height); | ||
const bounds = getBoundsForDisplayArea(displayArea); | ||
const topPlacementBottomBound = anchorPoint.y - arrowSize.height; | ||
if (tooltipOrigin.x < bounds.x.min) { | ||
boundTooltipOrigin.x = bounds.x.min; | ||
} | ||
if (tooltipOrigin.y < bounds.y.min) { | ||
boundTooltipOrigin.y = bounds.y.min; | ||
} | ||
if (boundTooltipOrigin.x + contentSize.width > bounds.x.max) { | ||
boundContentSize.width = bounds.x.max - boundTooltipOrigin.x; | ||
} | ||
if (boundTooltipOrigin.y + contentSize.height > topPlacementBottomBound) { | ||
boundContentSize.height = topPlacementBottomBound - boundTooltipOrigin.y; | ||
} | ||
return { | ||
@@ -73,12 +50,18 @@ tooltipOrigin, | ||
placement: 'top', | ||
boundContentSize, | ||
boundTooltipOrigin, | ||
}; | ||
}; | ||
const computeBottomGeometry = ({ displayArea, childRect, contentSize, arrowSize }) => { | ||
const computeBottomGeometry = ({ | ||
displayArea, | ||
childRect, | ||
contentSize, | ||
arrowSize, | ||
}) => { | ||
const tooltipOrigin = new Point( | ||
Math.min( | ||
displayArea.x + displayArea.width - contentSize.width, | ||
Math.max(displayArea.x, childRect.x + (childRect.width - contentSize.width) / 2), | ||
Math.max( | ||
displayArea.x, | ||
childRect.x + (childRect.width - contentSize.width) / 2, | ||
), | ||
), | ||
@@ -92,19 +75,2 @@ childRect.y + childRect.height + arrowSize.height, | ||
// compute bound content size | ||
const boundTooltipOrigin = new Point(tooltipOrigin.x, tooltipOrigin.y); | ||
const boundContentSize = new Size(contentSize.width, contentSize.height); | ||
const bounds = getBoundsForDisplayArea(displayArea); | ||
if (tooltipOrigin.x < bounds.x.min) { | ||
boundTooltipOrigin.x = bounds.x.min; | ||
} | ||
if (boundTooltipOrigin.x + contentSize.width > bounds.x.max) { | ||
boundContentSize.width = bounds.x.max - boundTooltipOrigin.x; | ||
} | ||
if (boundTooltipOrigin.y + contentSize.height > bounds.y.max) { | ||
boundContentSize.height = bounds.y.max - boundTooltipOrigin.y; | ||
} | ||
return { | ||
@@ -114,8 +80,11 @@ tooltipOrigin, | ||
placement: 'bottom', | ||
boundContentSize, | ||
boundTooltipOrigin, | ||
}; | ||
}; | ||
const computeLeftGeometry = ({ displayArea, childRect, contentSize, arrowSize }) => { | ||
const computeLeftGeometry = ({ | ||
displayArea, | ||
childRect, | ||
contentSize, | ||
arrowSize, | ||
}) => { | ||
const tooltipOrigin = new Point( | ||
@@ -125,30 +94,13 @@ childRect.x - contentSize.width - arrowSize.width, | ||
displayArea.y + displayArea.height - contentSize.height, | ||
Math.max(displayArea.y, childRect.y + (childRect.height - contentSize.height) / 2), | ||
Math.max( | ||
displayArea.y, | ||
childRect.y + (childRect.height - contentSize.height) / 2, | ||
), | ||
), | ||
); | ||
const anchorPoint = new Point(childRect.x, childRect.y + childRect.height / 2.0); | ||
const anchorPoint = new Point( | ||
childRect.x, | ||
childRect.y + childRect.height / 2.0, | ||
); | ||
// compute bound content size | ||
const boundTooltipOrigin = new Point(tooltipOrigin.x, tooltipOrigin.y); | ||
const boundContentSize = new Size(contentSize.width, contentSize.height); | ||
const bounds = getBoundsForDisplayArea(displayArea); | ||
const leftPlacementRightBound = anchorPoint.x - arrowSize.width; | ||
if (tooltipOrigin.x < bounds.x.min) { | ||
boundTooltipOrigin.x = bounds.x.min; | ||
} | ||
if (tooltipOrigin.y < bounds.y.min) { | ||
boundTooltipOrigin.y = bounds.y.min; | ||
} | ||
if (boundTooltipOrigin.x + contentSize.width > leftPlacementRightBound) { | ||
boundContentSize.width = leftPlacementRightBound - boundTooltipOrigin.x; | ||
} | ||
if (boundTooltipOrigin.y + contentSize.height > bounds.y.max) { | ||
boundContentSize.height = bounds.y.max - boundTooltipOrigin.y; | ||
} | ||
return { | ||
@@ -158,8 +110,11 @@ tooltipOrigin, | ||
placement: 'left', | ||
boundContentSize, | ||
boundTooltipOrigin, | ||
}; | ||
}; | ||
const computeRightGeometry = ({ displayArea, childRect, contentSize, arrowSize }) => { | ||
const computeRightGeometry = ({ | ||
displayArea, | ||
childRect, | ||
contentSize, | ||
arrowSize, | ||
}) => { | ||
const tooltipOrigin = new Point( | ||
@@ -169,3 +124,6 @@ childRect.x + childRect.width + arrowSize.width, | ||
displayArea.y + displayArea.height - contentSize.height, | ||
Math.max(displayArea.y, childRect.y + (childRect.height - contentSize.height) / 2), | ||
Math.max( | ||
displayArea.y, | ||
childRect.y + (childRect.height - contentSize.height) / 2, | ||
), | ||
), | ||
@@ -178,25 +136,2 @@ ); | ||
// compute bound content size | ||
const boundTooltipOrigin = new Point(tooltipOrigin.x, tooltipOrigin.y); | ||
const boundContentSize = new Size(contentSize.width, contentSize.height); | ||
const bounds = getBoundsForDisplayArea(displayArea); | ||
const rightPlacementLeftBound = anchorPoint.x + arrowSize.width; | ||
if (tooltipOrigin.x < rightPlacementLeftBound) { | ||
boundTooltipOrigin.x = rightPlacementLeftBound; | ||
} | ||
if (tooltipOrigin.y < bounds.y.min) { | ||
boundTooltipOrigin.y = bounds.y.min; | ||
} | ||
if (boundTooltipOrigin.x + contentSize.width > bounds.x.max) { | ||
boundContentSize.width = bounds.x.max - boundTooltipOrigin.x; | ||
} | ||
if (boundTooltipOrigin.y + contentSize.height > bounds.y.max) { | ||
boundContentSize.height = bounds.y.max - boundTooltipOrigin.y; | ||
} | ||
return { | ||
@@ -206,4 +141,2 @@ tooltipOrigin, | ||
placement: 'right', | ||
boundContentSize, | ||
boundTooltipOrigin, | ||
}; | ||
@@ -210,0 +143,0 @@ }; |
@@ -9,3 +9,2 @@ import React, { Component } from 'react'; | ||
Modal, | ||
Platform, | ||
StyleSheet, | ||
@@ -15,3 +14,2 @@ TouchableWithoutFeedback, | ||
} from 'react-native'; | ||
import rfcIsEqual from 'react-fast-compare'; | ||
import { | ||
@@ -27,2 +25,3 @@ Point, | ||
import styles from './styles'; | ||
import TooltipChildrenContext from './tooltip-children.context'; | ||
@@ -34,8 +33,2 @@ const SCREEN_HEIGHT = Dimensions.get('window').height; | ||
const DEFAULT_PADDING = 24; | ||
const DEFAULT_DISPLAY_AREA = new Rect( | ||
DEFAULT_PADDING, | ||
DEFAULT_PADDING, | ||
SCREEN_WIDTH - DEFAULT_PADDING * 2, | ||
SCREEN_HEIGHT - DEFAULT_PADDING * 2, | ||
); | ||
@@ -57,2 +50,5 @@ const invertPlacement = (placement) => { | ||
const TooltipChildrenProvider = TooltipChildrenContext.Provider; | ||
export const TooltipChildrenConsumer = TooltipChildrenContext.Consumer; | ||
class Tooltip extends Component { | ||
@@ -66,4 +62,8 @@ static defaultProps = { | ||
content: <View />, | ||
contentBoundByDisplayArea: true, | ||
displayArea: DEFAULT_DISPLAY_AREA, | ||
displayArea: new Rect( | ||
DEFAULT_PADDING, | ||
DEFAULT_PADDING, | ||
SCREEN_WIDTH - DEFAULT_PADDING * 2, | ||
SCREEN_HEIGHT - DEFAULT_PADDING * 2, | ||
), | ||
isVisible: false, | ||
@@ -74,48 +74,14 @@ onChildLongPress: null, | ||
placement: 'auto', | ||
rotationDeg: 0, | ||
useInteractionManager: false, | ||
}; | ||
static propTypes = { | ||
animated: PropTypes.bool, | ||
arrowSize: PropTypes.shape({ | ||
height: PropTypes.number, | ||
width: PropTypes.number, | ||
}), | ||
backgroundColor: PropTypes.string, | ||
childlessPlacementPadding: PropTypes.oneOfType([ | ||
PropTypes.number, | ||
PropTypes.string, | ||
]), | ||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), | ||
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), | ||
contentBoundByDisplayArea: PropTypes.bool, | ||
displayArea: PropTypes.shape({ | ||
x: PropTypes.number, | ||
y: PropTypes.number, | ||
height: PropTypes.number, | ||
width: PropTypes.number, | ||
}), | ||
isVisible: PropTypes.bool, | ||
onChildLongPress: PropTypes.func, | ||
onChildPress: PropTypes.func, | ||
onClose: PropTypes.func, | ||
placement: PropTypes.oneOf(['top', 'left', 'bottom', 'right', 'auto']), | ||
rotationDeg: PropTypes.oneOf([0, 90, 180, 270]), | ||
useInteractionManager: PropTypes.bool, | ||
}; | ||
constructor(props) { | ||
super(props); | ||
const { isVisible, useInteractionManager } = props; | ||
const { isVisible } = props; | ||
this.isMeasuringChild = false; | ||
this.childWrapper = React.createRef(); | ||
this.state = { | ||
// no need to wait for interactions if not visible initially | ||
waitingForInteractions: isVisible && useInteractionManager, | ||
waitingForInteractions: isVisible, | ||
contentSize: new Size(0, 0), | ||
boundContentSize: new Size(0, 0), | ||
anchorPoint: new Point(0, 0), | ||
@@ -126,5 +92,3 @@ tooltipOrigin: new Point(0, 0), | ||
// behave like placement "bottom", i.e. display below the top of the screen | ||
placement: !props.children | ||
? invertPlacement(props.placement) | ||
: props.placement, | ||
placement: !props.children ? invertPlacement(props.placement) : props.placement, | ||
readyToComputeGeom: false, | ||
@@ -143,32 +107,33 @@ waitingToComputeGeom: false, | ||
if (this.state.waitingForInteractions) { | ||
this.measureChildRect(); | ||
InteractionManager.runAfterInteractions(() => { | ||
this.measureChildRect(); | ||
this.setState({ waitingForInteractions: false }); | ||
}); | ||
} | ||
} | ||
componentDidUpdate(prevProps) { | ||
const { content, isVisible, placement, rotationDeg } = this.props; | ||
componentWillReceiveProps(nextProps) { | ||
const willBeVisible = nextProps.isVisible; | ||
const nextContent = nextProps.content; | ||
const { isVisible, content } = this.props; | ||
const contentChanged = !rfcIsEqual(prevProps.content, content); | ||
const placementChanged = prevProps.placement !== placement; | ||
const rotationChanged = prevProps.rotationDeg !== rotationDeg; | ||
const becameVisible = isVisible && !prevProps.isVisible; | ||
const becameHidden = !isVisible && prevProps.isVisiblel; | ||
if (becameHidden) { | ||
this._startAnimation({ show: false }); | ||
} else if ( | ||
contentChanged || | ||
placementChanged || | ||
rotationChanged || | ||
becameVisible | ||
) { | ||
setTimeout(() => { | ||
if (nextContent !== content && willBeVisible) { | ||
// The location of the child element may have changed based on | ||
// transition animations in the corresponding view, so remeasure | ||
InteractionManager.runAfterInteractions(() => { | ||
this.measureChildRect(); | ||
}); | ||
if (becameVisible) { | ||
// TODO: Move setState out of didUpdate | ||
} else if (willBeVisible !== isVisible) { | ||
if (willBeVisible) { | ||
// We want to start the show animation only when contentSize is known | ||
// so that we can have some logic depending on the geometry | ||
this.setState({ contentSize: new Size(0, 0) }); | ||
// The location of the child element may have changed based on | ||
// transition animations in the corresponding view, so remeasure | ||
InteractionManager.runAfterInteractions(() => { | ||
this.measureChildRect(); | ||
}); | ||
} else { | ||
this._startAnimation({ show: false }); | ||
} | ||
@@ -178,11 +143,8 @@ } | ||
static getDerivedStateFromProps(nextProps, prevState) { | ||
// set measurements finished flag to false when tooltip closes | ||
if (prevState.measurementsFinished && !nextProps.isVisible) { | ||
return { | ||
measurementsFinished: false, | ||
}; | ||
componentDidUpdate() { | ||
// We always want the measurements finished flag to be false | ||
// after the tooltip is closed | ||
if (this.state.measurementsFinished && !this.props.isVisible) { | ||
this.setState({ measurementsFinished: false }); | ||
} | ||
return null; | ||
} | ||
@@ -249,86 +211,2 @@ | ||
getPlacementForCurrentRotation = () => { | ||
const { placement: basePlacement, rotationDeg } = this.props; | ||
switch (basePlacement) { | ||
case 'top': | ||
switch (rotationDeg) { | ||
case 90: | ||
return 'right'; | ||
case 180: | ||
return 'bottom'; | ||
case 270: | ||
return 'left'; | ||
case 0: | ||
default: | ||
return 'top'; | ||
} | ||
case 'left': | ||
switch (rotationDeg) { | ||
case 90: | ||
return 'top'; | ||
case 180: | ||
return 'right'; | ||
case 270: | ||
return 'bottom'; | ||
case 0: | ||
default: | ||
return 'left'; | ||
} | ||
case 'bottom': | ||
switch (rotationDeg) { | ||
case 90: | ||
return 'left'; | ||
case 180: | ||
return 'top'; | ||
case 270: | ||
return 'right'; | ||
case 0: | ||
default: | ||
return 'bottom'; | ||
} | ||
case 'right': | ||
switch (rotationDeg) { | ||
case 90: | ||
return 'bottom'; | ||
case 180: | ||
return 'left'; | ||
case 270: | ||
return 'top'; | ||
case 0: | ||
default: | ||
return 'right'; | ||
} | ||
default: | ||
return basePlacement; | ||
} | ||
}; | ||
getTranslationForCurrentRotation = () => { | ||
const { placement: basePlacement, rotationDeg } = this.props; | ||
const { | ||
width: contentWidth, | ||
height: contentHeight, | ||
} = this.state.contentSize; | ||
const offset = Math.abs(contentWidth - contentHeight) / 2; | ||
if (rotationDeg % 180 !== 0) { | ||
switch (basePlacement) { | ||
case 'top': | ||
return { y: offset }; | ||
case 'left': | ||
return { x: -offset }; | ||
case 'bottom': | ||
return { y: -offset }; | ||
case 'right': | ||
return { x: offset }; | ||
default: | ||
break; | ||
} | ||
} | ||
return {}; | ||
}; | ||
getTooltipPlacementStyles = () => { | ||
@@ -374,8 +252,15 @@ const { height } = this.props.arrowSize; | ||
); | ||
return new Point( | ||
anchorPoint.x - tooltipCenter.x, | ||
anchorPoint.y - tooltipCenter.y, | ||
); | ||
return new Point(anchorPoint.x - tooltipCenter.x, anchorPoint.y - tooltipCenter.y); | ||
}; | ||
waitAndMeasureChildRect = (clearWaitingForInteractions) => { | ||
setTimeout(() => { | ||
this.measureChildRect(); | ||
if (clearWaitingForInteractions) { | ||
this.setState({ waitingForInteractions: false }); | ||
} | ||
}, 1000); | ||
}; | ||
measureContent = (e) => { | ||
@@ -393,6 +278,2 @@ const { width, height } = e.nativeEvent.layout; | ||
} | ||
if (!this.props.children) { | ||
this.mockChildRect(); | ||
} | ||
}; | ||
@@ -407,112 +288,73 @@ | ||
} | ||
this.setState({ measurementsFinished: true }); | ||
}; | ||
measureChildRect = () => { | ||
const doMeasurement = () => { | ||
if (!this.isMeasuringChild) { | ||
this.isMeasuringChild = true; | ||
if ( | ||
this.childWrapper.current && | ||
typeof this.childWrapper.current.measureInWindow === 'function' | ||
) { | ||
this.childWrapper.current.measureInWindow((x, y, width, height) => { | ||
this.setState( | ||
{ | ||
childRect: new Rect(x, y, width, height), | ||
readyToComputeGeom: true, | ||
}, | ||
() => this.finishMeasurements(), | ||
); | ||
}); | ||
} else { | ||
// mock the placement of a child to compute geom | ||
let rectForChildlessPlacement = { ...this.state.childRect }; | ||
let placementPadding = DEFAULT_PADDING; | ||
if ( | ||
this.childWrapper.current && | ||
typeof this.childWrapper.current.measureInWindow === 'function' | ||
) { | ||
this.childWrapper.current.measureInWindow((x, y, width, height) => { | ||
this.setState({ | ||
childRect: new Rect(x, y, width, height), | ||
readyToComputeGeom: true, | ||
waitingForInteractions: false, | ||
placement: this.getPlacementForCurrentRotation(), | ||
}, | ||
() => { | ||
this.isMeasuringChild = false; | ||
this.finishMeasurements(); | ||
}); | ||
}); | ||
const { childlessPlacementPadding, placement } = this.props; | ||
// handle percentages | ||
if (typeof childlessPlacementPadding === 'string') { | ||
const isPercentage = | ||
childlessPlacementPadding.substring(childlessPlacementPadding.length - 1) === '%'; | ||
const paddingValue = parseFloat(childlessPlacementPadding, 10); | ||
const verticalPlacement = placement === 'top' || placement === 'bottom'; | ||
if (isPercentage) { | ||
placementPadding = | ||
(paddingValue / 100.0) * (verticalPlacement ? SCREEN_HEIGHT : SCREEN_WIDTH); | ||
} else { | ||
this.mockChildRect(); | ||
placementPadding = paddingValue; | ||
} | ||
} else { | ||
placementPadding = childlessPlacementPadding; | ||
} | ||
}; | ||
if (this.props.useInteractionManager) { | ||
InteractionManager.runAfterInteractions(() => { | ||
doMeasurement(); | ||
}); | ||
} else { | ||
doMeasurement(); | ||
} | ||
}; | ||
if (Number.isNaN(placementPadding)) { | ||
throw new Error('[Tooltip] Invalid value passed to childlessPlacementPadding'); | ||
} | ||
mockChildRect = () => { | ||
// mock the placement of a child to compute geom | ||
let rectForChildlessPlacement = { ...this.state.childRect }; | ||
let placementPadding = DEFAULT_PADDING; | ||
const CENTER_X = SCREEN_WIDTH / 2; | ||
const CENTER_Y = SCREEN_HEIGHT / 2; | ||
const { childlessPlacementPadding, placement } = this.props; | ||
// handle percentages | ||
if (typeof childlessPlacementPadding === 'string') { | ||
const isPercentage = | ||
childlessPlacementPadding.substring( | ||
childlessPlacementPadding.length - 1, | ||
) === '%'; | ||
const paddingValue = parseFloat(childlessPlacementPadding, 10); | ||
const verticalPlacement = placement === 'top' || placement === 'bottom'; | ||
if (isPercentage) { | ||
placementPadding = | ||
(paddingValue / 100.0) * | ||
(verticalPlacement ? SCREEN_HEIGHT : SCREEN_WIDTH); | ||
} else { | ||
placementPadding = paddingValue; | ||
switch (placement) { | ||
case 'bottom': | ||
rectForChildlessPlacement = new Rect(CENTER_X, SCREEN_HEIGHT - placementPadding, 0, 0); | ||
break; | ||
case 'left': | ||
rectForChildlessPlacement = new Rect(placementPadding, CENTER_Y, 0, 0); | ||
break; | ||
case 'right': | ||
rectForChildlessPlacement = new Rect(SCREEN_WIDTH - placementPadding, CENTER_Y, 0, 0); | ||
break; | ||
default: | ||
case 'top': | ||
rectForChildlessPlacement = new Rect(CENTER_X, placementPadding, 0, 0); | ||
break; | ||
} | ||
} else { | ||
placementPadding = childlessPlacementPadding; | ||
} | ||
if (Number.isNaN(placementPadding)) { | ||
throw new Error( | ||
'[Tooltip] Invalid value passed to childlessPlacementPadding', | ||
this.setState( | ||
{ | ||
childRect: rectForChildlessPlacement, | ||
readyToComputeGeom: true, | ||
}, | ||
() => this.finishMeasurements(), | ||
); | ||
} | ||
const CENTER_X = SCREEN_WIDTH / 2; | ||
const CENTER_Y = SCREEN_HEIGHT / 2; | ||
switch (placement) { | ||
case 'bottom': | ||
rectForChildlessPlacement = new Rect( | ||
CENTER_X, | ||
SCREEN_HEIGHT - placementPadding, | ||
0, | ||
0, | ||
); | ||
break; | ||
case 'left': | ||
rectForChildlessPlacement = new Rect(placementPadding, CENTER_Y, 0, 0); | ||
break; | ||
case 'right': | ||
rectForChildlessPlacement = new Rect( | ||
SCREEN_WIDTH - placementPadding, | ||
CENTER_Y, | ||
0, | ||
0, | ||
); | ||
break; | ||
default: | ||
case 'top': | ||
rectForChildlessPlacement = new Rect(CENTER_X, placementPadding, 0, 0); | ||
break; | ||
} | ||
this.setState( | ||
{ | ||
childRect: rectForChildlessPlacement, | ||
readyToComputeGeom: true, | ||
}, | ||
() => { | ||
this.isMeasuringChild = false; | ||
this.finishMeasurements(); | ||
}, | ||
); | ||
}; | ||
@@ -522,10 +364,3 @@ | ||
const geom = this.computeGeometry({ contentSize }); | ||
const { | ||
tooltipOrigin, | ||
anchorPoint, | ||
placement, | ||
boundContentSize, | ||
boundTooltipOrigin, | ||
} = geom; | ||
const { contentBoundByDisplayArea } = this.props; | ||
const { tooltipOrigin, anchorPoint, placement } = geom; | ||
@@ -535,6 +370,3 @@ this.setState( | ||
contentSize, | ||
boundContentSize, | ||
tooltipOrigin: contentBoundByDisplayArea | ||
? boundTooltipOrigin | ||
: tooltipOrigin, | ||
tooltipOrigin, | ||
anchorPoint, | ||
@@ -544,3 +376,2 @@ placement, | ||
waitingToComputeGeom: false, | ||
measurementsFinished: true, | ||
}, | ||
@@ -555,19 +386,8 @@ () => { | ||
const geom = this.computeGeometry({ contentSize }); | ||
const { | ||
tooltipOrigin, | ||
anchorPoint, | ||
placement, | ||
boundContentSize, | ||
boundTooltipOrigin, | ||
} = geom; | ||
const { contentBoundByDisplayArea } = this.props; | ||
const { tooltipOrigin, anchorPoint, placement } = geom; | ||
this.setState({ | ||
boundContentSize, | ||
tooltipOrigin: contentBoundByDisplayArea | ||
? boundTooltipOrigin | ||
: tooltipOrigin, | ||
tooltipOrigin, | ||
anchorPoint, | ||
placement, | ||
measurementsFinished: true, | ||
}); | ||
@@ -614,7 +434,5 @@ }; | ||
tooltipOrigin.x >= displayArea.x && | ||
tooltipOrigin.x <= | ||
displayArea.x + displayArea.width - contentSize.width && | ||
tooltipOrigin.x <= displayArea.x + displayArea.width - contentSize.width && | ||
tooltipOrigin.y >= displayArea.y && | ||
tooltipOrigin.y <= | ||
displayArea.y + displayArea.height - contentSize.height | ||
tooltipOrigin.y <= displayArea.y + displayArea.height - contentSize.height | ||
) { | ||
@@ -688,4 +506,2 @@ break; | ||
_getExtendedStyles = () => { | ||
const { animated, rotationDeg } = this.props; | ||
const background = []; | ||
@@ -696,3 +512,3 @@ const tooltip = []; | ||
const animatedStyles = animated ? this._getDefaultAnimatedStyles() : null; | ||
const animatedStyles = this.props.animated ? this._getDefaultAnimatedStyles() : null; | ||
@@ -708,29 +524,2 @@ [animatedStyles, this.props].forEach((source) => { | ||
if (rotationDeg !== 0) { | ||
const translation = this.getTranslationForCurrentRotation(); | ||
const transformArray = []; // StyleSheet.flatten(content).transform; | ||
transformArray.push({ | ||
rotate: `${rotationDeg}deg`, | ||
}); | ||
if (translation.x) { | ||
transformArray.push({ | ||
translateX: translation.x, | ||
}); | ||
} | ||
if (translation.y) { | ||
transformArray.push({ | ||
translateY: translation.y, | ||
}); | ||
} | ||
if (Platform.OS === 'android') { | ||
tooltip.push({ transform: transformArray }); | ||
} else { | ||
content.push({ transform: transformArray }); | ||
} | ||
} | ||
return { | ||
@@ -748,4 +537,3 @@ background, | ||
const wrapInTouchable = | ||
typeof onChildPress === 'function' || | ||
typeof onChildLongPress === 'function'; | ||
typeof onChildPress === 'function' || typeof onChildLongPress === 'function'; | ||
@@ -765,3 +553,5 @@ const childElement = ( | ||
> | ||
{children} | ||
<TooltipChildrenProvider value={{ tooltipDuplicate: true }}> | ||
{children} | ||
</TooltipChildrenProvider> | ||
</View> | ||
@@ -772,6 +562,3 @@ ); | ||
return ( | ||
<TouchableWithoutFeedback | ||
onPress={onChildPress} | ||
onLongPress={onChildLongPress} | ||
> | ||
<TouchableWithoutFeedback onPress={onChildPress} onLongPress={onChildLongPress}> | ||
{childElement} | ||
@@ -786,46 +573,16 @@ </TouchableWithoutFeedback> | ||
render() { | ||
const { | ||
measurementsFinished, | ||
placement, | ||
waitingForInteractions, | ||
contentSize, | ||
boundContentSize, | ||
} = this.state; | ||
const { | ||
backgroundColor, | ||
children, | ||
content, | ||
isVisible, | ||
onClose, | ||
contentBoundByDisplayArea, | ||
rotationDeg, | ||
} = this.props; | ||
const { measurementsFinished, placement, waitingForInteractions } = this.state; | ||
const { backgroundColor, children, content, isVisible, onClose } = this.props; | ||
const sizeAvailable = contentBoundByDisplayArea | ||
? boundContentSize.width | ||
: contentSize.width; | ||
const extendedStyles = this._getExtendedStyles(); | ||
const contentStyle = [ | ||
styles.content, | ||
contentBoundByDisplayArea && boundContentSize.width | ||
? { ...boundContentSize } | ||
: {}, | ||
...extendedStyles.content, | ||
]; | ||
const contentStyle = [styles.content, ...extendedStyles.content]; | ||
const arrowColor = StyleSheet.flatten(contentStyle).backgroundColor; | ||
const arrowColorStyle = this.getArrowColorStyle(arrowColor); | ||
const arrowDynamicStyle = this.getArrowDynamicStyle(); | ||
const contentSizeAvailable = this.state.contentSize.width; | ||
const tooltipPlacementStyles = this.getTooltipPlacementStyles(); | ||
// Special case, force the arrow rotation even if it was overriden | ||
let arrowStyle = [ | ||
styles.arrow, | ||
arrowDynamicStyle, | ||
arrowColorStyle, | ||
...extendedStyles.arrow, | ||
]; | ||
const arrowTransform = ( | ||
StyleSheet.flatten(arrowStyle).transform || [] | ||
).slice(0); | ||
let arrowStyle = [styles.arrow, arrowDynamicStyle, arrowColorStyle, ...extendedStyles.arrow]; | ||
const arrowTransform = (StyleSheet.flatten(arrowStyle).transform || []).slice(0); | ||
arrowTransform.unshift({ rotate: this.getArrowRotation(placement) }); | ||
@@ -836,14 +593,6 @@ arrowStyle = [...arrowStyle, { transform: arrowTransform }]; | ||
// TODO: handle rotation better on android | ||
const tempHideArrowAndChild = | ||
rotationDeg !== 0 && Platform.OS === 'android'; | ||
return ( | ||
<View> | ||
{/* This renders the fullscreen tooltip */} | ||
<Modal | ||
transparent | ||
visible={isVisible && !waitingForInteractions} | ||
onRequestClose={onClose} | ||
> | ||
<Modal transparent visible={isVisible && !waitingForInteractions} onRequestClose={onClose}> | ||
<TouchableWithoutFeedback onPress={onClose}> | ||
@@ -853,34 +602,17 @@ <View | ||
styles.container, | ||
sizeAvailable && | ||
measurementsFinished && | ||
styles.containerVisible, | ||
contentSizeAvailable && measurementsFinished && styles.containerVisible, | ||
]} | ||
> | ||
<Animated.View | ||
style={[ | ||
styles.background, | ||
...extendedStyles.background, | ||
{ backgroundColor }, | ||
]} | ||
style={[styles.background, ...extendedStyles.background, { backgroundColor }]} | ||
/> | ||
<Animated.View | ||
style={[ | ||
styles.tooltip, | ||
...extendedStyles.tooltip, | ||
tooltipPlacementStyles, | ||
]} | ||
style={[styles.tooltip, ...extendedStyles.tooltip, tooltipPlacementStyles]} | ||
> | ||
{noChildren || tempHideArrowAndChild ? null : ( | ||
<Animated.View style={arrowStyle} /> | ||
)} | ||
<Animated.View | ||
onLayout={this.measureContent} | ||
style={contentStyle} | ||
> | ||
{noChildren ? null : <Animated.View style={arrowStyle} />} | ||
<Animated.View onLayout={this.measureContent} style={contentStyle}> | ||
{content} | ||
</Animated.View> | ||
</Animated.View> | ||
{noChildren || tempHideArrowAndChild | ||
? null | ||
: this.renderChildInTooltip()} | ||
{noChildren ? null : this.renderChildInTooltip()} | ||
</View> | ||
@@ -901,2 +633,25 @@ </TouchableWithoutFeedback> | ||
Tooltip.propTypes = { | ||
animated: PropTypes.bool, | ||
arrowSize: PropTypes.shape({ | ||
height: PropTypes.number, | ||
width: PropTypes.number, | ||
}), | ||
backgroundColor: PropTypes.string, | ||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), | ||
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), | ||
displayArea: PropTypes.shape({ | ||
x: PropTypes.number, | ||
y: PropTypes.number, | ||
height: PropTypes.number, | ||
width: PropTypes.number, | ||
}), | ||
isVisible: PropTypes.bool, | ||
onChildLongPress: PropTypes.func, | ||
onChildPress: PropTypes.func, | ||
onClose: PropTypes.func, | ||
placement: PropTypes.string, | ||
childlessPlacementPadding: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), | ||
}; | ||
export default Tooltip; |
10
25%109
19.78%39213
-6.32%830
-16.83%+ Added
+ Added
+ Added
+ Added
- Removed
- Removed