diff --git a/src/components/Tooltip/TooltipRenderedOnPageBody.js b/src/components/Tooltip/TooltipRenderedOnPageBody.js index 54095286313c..40dabf1b7651 100644 --- a/src/components/Tooltip/TooltipRenderedOnPageBody.js +++ b/src/components/Tooltip/TooltipRenderedOnPageBody.js @@ -4,6 +4,7 @@ import {Animated, View} from 'react-native'; import ReactDOM from 'react-dom'; import getTooltipStyles from '../../styles/getTooltipStyles'; import Text from '../Text'; +import Log from '../../libs/Log'; const propTypes = { /** Window width */ @@ -36,16 +37,21 @@ const propTypes = { /** Text to be shown in the tooltip */ text: PropTypes.string.isRequired, - /** Number of pixels to set max-width on tooltip */ - maxWidth: PropTypes.number.isRequired, - /** Maximum number of lines to show in tooltip */ numberOfLines: PropTypes.number.isRequired, + + /** Number of pixels to set max-width on tooltip */ + maxWidth: PropTypes.number, + + /** Render custom content inside the tooltip. Note: This cannot be used together with the text props. */ + renderTooltipContent: PropTypes.func, }; const defaultProps = { shiftHorizontal: 0, shiftVertical: 0, + renderTooltipContent: undefined, + maxWidth: 0, }; // Props will change frequently. @@ -57,35 +63,45 @@ class TooltipRenderedOnPageBody extends React.PureComponent { constructor(props) { super(props); this.state = { - // The width of tooltip's inner text - tooltipTextWidth: 0, + // The width of tooltip's inner content. Has to be undefined in the beginning + // as a width of 0 will cause the content to be rendered of a width of 0, + // which prevents us from measuring it correctly. + tooltipContentWidth: undefined, // The width and height of the tooltip itself tooltipWidth: 0, tooltipHeight: 0, }; + if (props.renderTooltipContent && props.text) { + Log.warn('Developer error: Cannot use both text and renderTooltipContent props at the same time in !'); + } + this.measureTooltip = this.measureTooltip.bind(this); - this.updateTooltipTextWidth = this.updateTooltipTextWidth.bind(this); + this.updateTooltipContentWidth = this.updateTooltipContentWidth.bind(this); } componentDidMount() { - this.updateTooltipTextWidth(); + this.updateTooltipContentWidth(); } componentDidUpdate(prevProps) { - if (prevProps.text === this.props.text) { + if (prevProps.text === this.props.text && prevProps.renderTooltipContent === this.props.renderTooltipContent) { return; } // Reset the tooltip text width to 0 so that we can measure it again. // eslint-disable-next-line react/no-did-update-set-state - this.setState({tooltipTextWidth: 0}, this.updateTooltipTextWidth); + this.setState({tooltipContentWidth: undefined}, this.updateTooltipContentWidth); } - updateTooltipTextWidth() { + updateTooltipContentWidth() { + if (!this.contentRef) { + return; + } + this.setState({ - tooltipTextWidth: this.textRef.offsetWidth, + tooltipContentWidth: this.contentRef.offsetWidth, }); } @@ -118,32 +134,45 @@ class TooltipRenderedOnPageBody extends React.PureComponent { this.props.maxWidth, this.state.tooltipWidth, this.state.tooltipHeight, - this.state.tooltipTextWidth, + this.state.tooltipContentWidth, this.props.shiftHorizontal, this.props.shiftVertical, ); + + const contentRef = (ref) => { + // Once the content for the tooltip first renders, update the width of the tooltip dynamically to fit the width of the content. + // Note that we can't have this code in componentDidMount because the ref for the content won't be set until after the first render + if (this.contentRef) { + return; + } + + this.contentRef = ref; + this.updateTooltipContentWidth(); + }; + + let content; + if (this.props.renderTooltipContent) { + content = ( + + {this.props.renderTooltipContent()} + + ); + } else { + content = ( + + + {this.props.text} + + + ); + } + return ReactDOM.createPortal( - - { - // Once the text for the tooltip first renders, update the width of the tooltip dynamically to fit the width of the text. - // Note that we can't have this code in componentDidMount because the ref for the text won't be set until after the first render - if (this.textRef) { - return; - } - - this.textRef = ref; - this.updateTooltipTextWidth(); - }} - > - {this.props.text} - - + {content} diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index 88c8d7401430..68dcd2e1cce9 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -142,8 +142,9 @@ class Tooltip extends PureComponent { } render() { - // Skip the tooltip and return the children if the text is empty or the device does not support hovering - if (_.isEmpty(this.props.text) || !this.hasHoverSupport) { + // Skip the tooltip and return the children if the text is empty, + // we don't have a render function or the device does not support hovering + if ((_.isEmpty(this.props.text) && this.props.renderTooltipContent == null) || !this.hasHoverSupport) { return this.props.children; } let child = ( @@ -180,6 +181,7 @@ class Tooltip extends PureComponent { focusable: true, }); } + return ( <> {this.state.isRendered && ( @@ -195,6 +197,7 @@ class Tooltip extends PureComponent { text={this.props.text} maxWidth={this.props.maxWidth} numberOfLines={this.props.numberOfLines} + renderTooltipContent={this.props.renderTooltipContent} /> )} ( +
+ {/* eslint-disable-next-line react/jsx-props-no-spreading */} + +
+ Hover me +
+
+
+); + +// Arguments can be passed to the component by binding +// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +const Default = Template.bind({}); +Default.args = { + text: 'Tooltip', + numberOfLines: 1, + maxWidth: 0, + absolute: false, +}; + +const RenderContent = () => { + const [size, setSize] = React.useState(40); + + const renderTooltipContent = () => ( +
+ ); + + return ( +
+ {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
setSize(size + 25)} + style={{ + width: 100, + height: 60, + display: 'flex', + backgroundColor: 'red', + justifyContent: 'center', + alignItems: 'center', + }} + > + Hover me + {' '} + {'\n'} + Press me change content +
+
+
+ ); +}; + +export default story; +export { + Default, + RenderContent, +}; diff --git a/src/styles/getTooltipStyles.js b/src/styles/getTooltipStyles.js index c2816665a45b..6087ca485c16 100644 --- a/src/styles/getTooltipStyles.js +++ b/src/styles/getTooltipStyles.js @@ -63,7 +63,7 @@ function computeHorizontalShift(windowWidth, xOffset, componentWidth, tooltipWid * @param {Number} maxWidth - The tooltip's max width. * @param {Number} tooltipWidth - The width of the tooltip itself. * @param {Number} tooltipHeight - The height of the tooltip itself. - * @param {Number} tooltipTextWidth - The tooltip's inner text width. + * @param {Number} tooltipContentWidth - The tooltip's inner content width. * @param {Number} [manualShiftHorizontal] - Any additional amount to manually shift the tooltip to the left or right. * A positive value shifts it to the right, * and a negative value shifts it to the left. @@ -81,7 +81,7 @@ export default function getTooltipStyles( maxWidth, tooltipWidth, tooltipHeight, - tooltipTextWidth, + tooltipContentWidth, manualShiftHorizontal = 0, manualShiftVertical = 0, ) { @@ -99,10 +99,10 @@ export default function getTooltipStyles( // We get wrapper width based on the tooltip's inner text width so the wrapper is just big enough to fit text and prevent white space. // If the text width is less than the maximum available width, add horizontal padding. - // Note: tooltipTextWidth ignores the fractions (OffsetWidth) so add 1px to fit the text properly. - const wrapperWidth = tooltipTextWidth && tooltipTextWidth < maxWidth - ? tooltipTextWidth + (spacing.ph2.paddingHorizontal * 2) + 1 - : maxWidth; + // Note: tooltipContentWidth ignores the fractions (OffsetWidth) so add 1px to fit the text properly. + const wrapperWidth = tooltipContentWidth && (tooltipContentWidth < maxWidth + ? tooltipContentWidth + (spacing.ph2.paddingHorizontal * 2) + 1 + : maxWidth); // Hide the tooltip entirely if it's position hasn't finished measuring yet. This prevents UI jank where the tooltip flashes in the top left corner of the screen. const opacity = (xOffset === 0 && yOffset === 0) ? 0 : 1;