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
76 changes: 59 additions & 17 deletions src/components/Tooltip/TooltipRenderedOnPageBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import {Animated, View} from 'react-native';
import ReactDOM from 'react-dom';
import _ from 'underscore';
import getTooltipStyles from '../../styles/getTooltipStyles';
import Text from '../Text';
import styles from '../../styles/styles';

const propTypes = {
/** Window width */
Expand Down Expand Up @@ -36,16 +38,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,15 +64,16 @@ 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: 0,

// The width and height of the tooltip itself
tooltipWidth: 0,
tooltipHeight: 0,
};

this.measureTooltip = this.measureTooltip.bind(this);
this.measureContent = this.measureContent.bind(this);
this.updateTooltipTextWidth = this.updateTooltipTextWidth.bind(this);
}

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 All @@ -101,6 +113,12 @@ class TooltipRenderedOnPageBody extends React.PureComponent {
});
}

measureContent({nativeEvent}) {
this.setState({
tooltipContentWidth: nativeEvent.layout.width,
});
}

hannojg marked this conversation as resolved.
Show resolved Hide resolved
render() {
const {
animationStyle,
Expand All @@ -118,15 +136,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,10 +163,33 @@ class TooltipRenderedOnPageBody extends React.PureComponent {
{this.props.text}
</Text>
</Text>
<View style={pointerWrapperStyle}>
<View style={pointerStyle} />
</View>
</Animated.View>,
);
}

const isCustomContent = _.isFunction(this.props.renderTooltipContent);

return ReactDOM.createPortal(
<>
{/* If rendering custom content always render an invisible version of
it to detect layout size changes, if the content updates. */}
{isCustomContent && (
<View
style={styles.invisible}
onLayout={this.measureContent}
>
{this.props.renderTooltipContent()}
</View>
)}
hannojg marked this conversation as resolved.
Show resolved Hide resolved
<Animated.View
onLayout={this.measureTooltip}
style={[tooltipWrapperStyle, animationStyle]}
>
{content}
<View style={pointerWrapperStyle}>
<View style={pointerStyle} />
</View>
</Animated.View>
</>,
document.querySelector('body'),
);
}
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
95 changes: 95 additions & 0 deletions src/stories/Tooltip.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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>
);
};
RenderContent.args = {

};
hannojg marked this conversation as resolved.
Show resolved Hide resolved

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