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
87 changes: 58 additions & 29 deletions src/components/Tooltip/TooltipRenderedOnPageBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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. */
hannojg marked this conversation as resolved.
Show resolved Hide resolved
renderTooltipContent: PropTypes.func,
};

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

// Props will change frequently.
Expand All @@ -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,
hannojg marked this conversation as resolved.
Show resolved Hide resolved

// 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 <TooltipRenderedOnPageBody />!');
}

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() {
hannojg marked this conversation as resolved.
Show resolved Hide resolved
if (!this.contentRef) {
return;
}

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

Expand Down Expand Up @@ -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 = (
<View ref={contentRef}>
{this.props.renderTooltipContent()}
</View>
);
} else {
content = (
<Text numberOfLines={this.props.numberOfLines} style={tooltipTextStyle}>
<Text style={tooltipTextStyle} ref={contentRef}>
{this.props.text}
</Text>
</Text>
);
}

return ReactDOM.createPortal(
<Animated.View
onLayout={this.measureTooltip}
style={[tooltipWrapperStyle, animationStyle]}
>
<Text numberOfLines={this.props.numberOfLines} style={tooltipTextStyle}>
<Text
style={tooltipTextStyle}
ref={(ref) => {
// 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}
</Text>
</Text>
{content}
<View style={pointerWrapperStyle}>
<View style={pointerStyle} />
</View>
Expand Down
7 changes: 5 additions & 2 deletions src/components/Tooltip/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -180,6 +181,7 @@ class Tooltip extends PureComponent {
focusable: true,
});
}

return (
<>
{this.state.isRendered && (
Expand All @@ -195,6 +197,7 @@ class Tooltip extends PureComponent {
text={this.props.text}
maxWidth={this.props.maxWidth}
numberOfLines={this.props.numberOfLines}
renderTooltipContent={this.props.renderTooltipContent}
/>
)}
<Hoverable
Expand Down
8 changes: 6 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 All @@ -42,6 +45,7 @@ const defaultProps = {
text: '',
maxWidth: variables.sideBarWidth,
numberOfLines: CONST.TOOLTIP_MAX_LINES,
renderTooltipContent: undefined,
};

export {
Expand Down
92 changes: 92 additions & 0 deletions src/stories/Tooltip.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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 = () => {
const [size, setSize] = React.useState(40);

const renderTooltipContent = () => (
<div style={{
width: size,
height: size,
backgroundColor: 'blue',
}}
/>
);

return (
<div style={{
width: 100,
}}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<Tooltip renderTooltipContent={renderTooltipContent}>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div
onClick={() => setSize(size + 25)}
style={{
width: 100,
height: 60,
display: 'flex',
backgroundColor: 'red',
justifyContent: 'center',
alignItems: 'center',
}}
>
Hover me
{' '}
{'\n'}
Press me change content
</div>
</Tooltip>
</div>
);
};

export default story;
export {
Default,
RenderContent,
};
12 changes: 6 additions & 6 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,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;
Expand Down