Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Web] Change tooltip component to accept generic content #15325

Merged
46 changes: 34 additions & 12 deletions src/components/Tooltip/TooltipRenderedOnPageBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,24 @@ const propTypes = {
/** Text to be shown in the tooltip */
text: PropTypes.string.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.isRequired,

/** Maximum number of lines to show in tooltip */
numberOfLines: PropTypes.number.isRequired,
/** Render custom content inside the tooltip. Note: This cannot be used together with the text props. */
hannojg marked this conversation as resolved.
Show resolved Hide resolved
renderTooltipContent: PropTypes.func,

/** When rendering arbitrarily content using renderTooltipContent you should specify the width of the content */
contentWidth: PropTypes.number,
};

const defaultProps = {
shiftHorizontal: 0,
shiftVertical: 0,
contentWidth: 0,
renderTooltipContent: undefined,
};

// Props will change frequently.
Expand All @@ -57,8 +65,8 @@ 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
tooltipContentWidth: props.contentWidth,

// The width and height of the tooltip itself
tooltipWidth: 0,
Expand All @@ -80,12 +88,16 @@ class TooltipRenderedOnPageBody extends React.PureComponent {

// 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: 0}, this.updateTooltipTextWidth);
}

updateTooltipTextWidth() {
if (!this.textRef) {
return;
}

this.setState({
tooltipTextWidth: this.textRef.offsetWidth,
tooltipContentWidth: this.textRef.offsetWidth,
});
}

Expand Down Expand Up @@ -118,15 +130,16 @@ 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,
);
return ReactDOM.createPortal(
<Animated.View
onLayout={this.measureTooltip}
style={[tooltipWrapperStyle, animationStyle]}
>

let content;
if (this.props.renderTooltipContent) {
content = this.props.renderTooltipContent();
} else {
content = (
<Text numberOfLines={this.props.numberOfLines} style={tooltipTextStyle}>
<Text
style={tooltipTextStyle}
Expand All @@ -144,6 +157,15 @@ class TooltipRenderedOnPageBody extends React.PureComponent {
{this.props.text}
</Text>
</Text>
);
}

return ReactDOM.createPortal(
<Animated.View
onLayout={this.measureTooltip}
style={[tooltipWrapperStyle, animationStyle]}
>
{content}
<View style={pointerWrapperStyle}>
<View style={pointerStyle} />
</View>
Expand Down
32 changes: 30 additions & 2 deletions src/components/Tooltip/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {propTypes, defaultProps} from './tooltipPropTypes';
import TooltipSense from './TooltipSense';
import makeCancellablePromise from '../../libs/MakeCancellablePromise';
import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
import styles from '../../styles/styles';

class Tooltip extends PureComponent {
constructor(props) {
Expand All @@ -26,6 +27,9 @@ class Tooltip extends PureComponent {
// The width and height of the wrapper view
wrapperWidth: 0,
wrapperHeight: 0,

contentWidth: 0,
contentHeight: 0,
};

// Whether the tooltip is first tooltip to activate the TooltipSense
Expand All @@ -37,6 +41,7 @@ class Tooltip extends PureComponent {
this.getWrapperPosition = this.getWrapperPosition.bind(this);
this.showTooltip = this.showTooltip.bind(this);
this.hideTooltip = this.hideTooltip.bind(this);
this.measureContent = this.measureContent.bind(this);
}

componentDidUpdate(prevProps) {
Expand Down Expand Up @@ -141,9 +146,17 @@ class Tooltip extends PureComponent {
TooltipSense.deactivate();
}

measureContent({nativeEvent}) {
this.setState({
contentWidth: nativeEvent.layout.width,
contentHeight: nativeEvent.layout.height,
});
}

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 = (
Expand Down Expand Up @@ -180,8 +193,20 @@ class Tooltip extends PureComponent {
focusable: true,
});
}

const isCustomContent = _.isFunction(this.props.renderTooltipContent);
const isContentMeasured = this.state.contentWidth > 0 && this.state.contentHeight > 0;

return (
<>
{isCustomContent && !isContentMeasured && (
<View
style={styles.invisible}
onLayout={this.measureContent}
>
{this.props.renderTooltipContent()}
</View>
)}
hannojg marked this conversation as resolved.
Show resolved Hide resolved
{this.state.isRendered && (
<TooltipRenderedOnPageBody
animation={this.animation}
Expand All @@ -195,6 +220,9 @@ class Tooltip extends PureComponent {
text={this.props.text}
maxWidth={this.props.maxWidth}
numberOfLines={this.props.numberOfLines}
contentWidth={this.state.contentWidth}
contentHeight={this.state.contentHeight}
renderTooltipContent={this.props.renderTooltipContent}
/>
)}
<Hoverable
Expand Down
7 changes: 5 additions & 2 deletions src/components/Tooltip/tooltipPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const propTypes = {
/** The text to display in the tooltip. */
text: PropTypes.string,

/** Maximum number of lines to show in tooltip */
numberOfLines: PropTypes.number,

/** Styles to be assigned to the Tooltip wrapper views */
containerStyles: PropTypes.arrayOf(PropTypes.object),

Expand All @@ -30,8 +33,8 @@ const propTypes = {
/** Number of pixels to set max-width on tooltip */
maxWidth: PropTypes.number,

/** Maximum number of lines to show in tooltip */
numberOfLines: PropTypes.number,
/** Render custom content inside the tooltip. Note: This cannot be used together with the text props. */
renderTooltipContent: PropTypes.func,
};

const defaultProps = {
hannojg marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
63 changes: 63 additions & 0 deletions src/stories/Tooltip.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import Tooltip from '../components/Tooltip';

/**
* We use the Component Story Format for writing stories. Follow the docs here:
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
const story = {
title: 'Components/Tooltip',
component: Tooltip,
};

// eslint-disable-next-line react/jsx-props-no-spreading
const Template = args => (
<div style={{
width: 100,
}}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<Tooltip {...args} maxWidth={args.maxWidth || undefined}>
<div style={{
width: 100,
height: 60,
display: 'flex',
backgroundColor: 'red',
justifyContent: 'center',
alignItems: 'center',
}}
>
Hover me
</div>
</Tooltip>
</div>
);

// 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 = Template.bind({});
RenderContent.args = {
renderTooltipContent: () => (
<div style={{
width: 40,
height: 40,
backgroundColor: 'blue',
}}
/>
),
};

export default story;
export {
Default,
RenderContent,
};
10 changes: 5 additions & 5 deletions src/styles/getTooltipStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -81,7 +81,7 @@ export default function getTooltipStyles(
maxWidth,
tooltipWidth,
tooltipHeight,
tooltipTextWidth,
tooltipContentWidth,
manualShiftHorizontal = 0,
manualShiftVertical = 0,
) {
Expand All @@ -99,9 +99,9 @@ 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
// 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.
Expand Down