Skip to content

Commit

Permalink
Merge pull request #15325 from margelo/hanno/change-tooltip-accept-ge…
Browse files Browse the repository at this point in the history
…neric-content

[Web] Change tooltip component to accept generic content
  • Loading branch information
stitesExpensify authored Mar 7, 2023
2 parents 74f4e3d + 9863204 commit 28d15b1
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 39 deletions.
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. */
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,

// 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() {
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 = {
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

0 comments on commit 28d15b1

Please sign in to comment.