Skip to content

Commit

Permalink
Merge pull request #1632 from parasharrajat/parasharrajat/tooltip
Browse files Browse the repository at this point in the history
Added Tooltips to the report users' titles.
  • Loading branch information
tgolen authored Mar 19, 2021
2 parents 0d30a05 + 996ae0e commit 3431bf0
Show file tree
Hide file tree
Showing 26 changed files with 565 additions and 130 deletions.
5 changes: 5 additions & 0 deletions src/components/Hoverable/HoverablePropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ const propTypes = {
PropTypes.func,
]).isRequired,

// Styles to be assigned to the Hoverable Container
// eslint-disable-next-line react/forbid-prop-types
containerStyle: PropTypes.object,

// Function that executes when the mouse moves over the children.
onHoverIn: PropTypes.func,

Expand All @@ -15,6 +19,7 @@ const propTypes = {
};

const defaultProps = {
containerStyle: {},
onHoverIn: () => {},
onHoverOut: () => {},
};
Expand Down
1 change: 1 addition & 0 deletions src/components/Hoverable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Hoverable extends Component {
render() {
return (
<View
style={this.props.containerStyle}
ref={el => this.wrapperView = el}
onMouseEnter={() => this.setIsHovered(true)}
onMouseLeave={() => this.setIsHovered(false)}
Expand Down
5 changes: 5 additions & 0 deletions src/components/OptionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ const propTypes = {
PropTypes.func,
PropTypes.shape({current: PropTypes.instanceOf(SectionList)}),
]),

// Whether to show the title tooltip
showTitleTooltip: PropTypes.bool,
};

const defaultProps = {
Expand All @@ -77,6 +80,7 @@ const defaultProps = {
onSelectRow: () => {},
headerMessage: '',
innerRef: null,
showTitleTooltip: false,
};

class OptionsList extends Component {
Expand Down Expand Up @@ -142,6 +146,7 @@ class OptionsList extends Component {
return (
<OptionRow
option={item}
showTitleTooltip={this.props.showTitleTooltip}
hoverStyle={this.props.optionHoveredStyle}
optionIsFocused={!this.props.disableFocusOptions
&& this.props.focusedIndex === (index + section.indexOffset)}
Expand Down
5 changes: 5 additions & 0 deletions src/components/OptionsSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ const propTypes = {

// Force the text style to be the unread style on all rows
forceTextUnreadStyle: PropTypes.bool,

// Whether to show the title tooltip
showTitleTooltip: PropTypes.bool,
};

const defaultProps = {
Expand All @@ -68,6 +71,7 @@ const defaultProps = {
disableArrowKeysActions: false,
hideAdditionalOptionStates: false,
forceTextUnreadStyle: false,
showTitleTooltip: false,
};

class OptionsSelector extends Component {
Expand Down Expand Up @@ -191,6 +195,7 @@ class OptionsSelector extends Component {
disableFocusOptions={this.props.disableArrowKeysActions}
hideAdditionalOptionStates={this.props.hideAdditionalOptionStates}
forceTextUnreadStyle={this.props.forceTextUnreadStyle}
showTitleTooltip={this.props.showTitleTooltip}
/>
</View>
);
Expand Down
34 changes: 34 additions & 0 deletions src/components/Tooltip/TooltipPropTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import PropTypes from 'prop-types';
import {windowDimensionsPropTypes} from '../withWindowDimensions';

const propTypes = {
// The text to display in the tooltip.
text: PropTypes.string.isRequired,

// Styles to be assigned to the Tooltip wrapper views
containerStyle: PropTypes.object,

// Children to wrap with Tooltip.
children: PropTypes.node.isRequired,

// Props inherited from withWindowDimensions
...windowDimensionsPropTypes,

// Any additional amount to manually adjust the horizontal position of the tooltip.
// A positive value shifts the tooltip to the right, and a negative value shifts it to the left.
shiftHorizontal: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),

// Any additional amount to manually adjust the vertical position of the tooltip.
// A positive value shifts the tooltip down, and a negative value shifts it up.
shiftVertical: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),
};

const defaultProps = {
shiftHorizontal: 0,
shiftVertical: 0,
};

export {
propTypes,
defaultProps,
};
62 changes: 62 additions & 0 deletions src/components/Tooltip/TooltipRenderedOnPageBody.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, {memo} from 'react';
import PropTypes from 'prop-types';
import {Animated, Text, View} from 'react-native';
import ReactDOM from 'react-dom';

const propTypes = {
// Style for Animation
// eslint-disable-next-line react/forbid-prop-types
animationStyle: PropTypes.object.isRequired,

// Syle for Tooltip wrapper
// eslint-disable-next-line react/forbid-prop-types
tooltipWrapperStyle: PropTypes.object.isRequired,

// Style for the text rendered inside tooltip
// eslint-disable-next-line react/forbid-prop-types
tooltipTextStyle: PropTypes.object.isRequired,

// Style for the Tooltip pointer Wrapper
// eslint-disable-next-line react/forbid-prop-types
pointerWrapperStyle: PropTypes.object.isRequired,

// Style for the Tooltip pointer
// eslint-disable-next-line react/forbid-prop-types
pointerStyle: PropTypes.object.isRequired,

// Callback to set the Ref to the Tooltip
setTooltipRef: PropTypes.func.isRequired,

// Text to be shown in the tooltip
text: PropTypes.string.isRequired,

// Callback to be used to calulate the width and height of tooltip
measureTooltip: PropTypes.func.isRequired,
};

const defaultProps = {};

const TooltipRenderedOnPageBody = props => ReactDOM.createPortal(
<Animated.View
ref={props.setTooltipRef}
onLayout={props.measureTooltip}
style={[props.tooltipWrapperStyle, props.animationStyle]}
>
<Text style={props.tooltipTextStyle} numberOfLines={1}>{props.text}</Text>
<View style={props.pointerWrapperStyle}>
<View style={props.pointerStyle} />
</View>
</Animated.View>,
document.querySelector('body'),
);

TooltipRenderedOnPageBody.propTypes = propTypes;
TooltipRenderedOnPageBody.defaultProps = defaultProps;
TooltipRenderedOnPageBody.displayName = 'TooltipRenderedOnPageBody';

// Props will change frequently.
// On every tooltip hover, we update the position in state which will result in re-rendering.
// We also update the state on layout changes which will be triggered often.
// There will be n number of tooltip components in the page.
// Its good to memorize this one.
export default memo(TooltipRenderedOnPageBody);
125 changes: 65 additions & 60 deletions src/components/Tooltip.js → src/components/Tooltip/index.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,11 @@
import _ from 'underscore';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Animated, Text, View} from 'react-native';
import Hoverable from './Hoverable';
import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
import getTooltipStyles from '../styles/getTooltipStyles';

const propTypes = {
// The text to display in the tooltip.
text: PropTypes.string.isRequired,

// Children to wrap with Tooltip.
children: PropTypes.node.isRequired,

// Props inherited from withWindowDimensions
...windowDimensionsPropTypes,

// Any additional amount to manually adjust the horizontal position of the tooltip.
// A positive value shifts the tooltip to the right, and a negative value shifts it to the left.
shiftHorizontal: PropTypes.number,

// Any additional amount to manually adjust the vertical position of the tooltip.
// A positive value shifts the tooltip down, and a negative value shifts it up.
shiftVertical: PropTypes.number,
};

const defaultProps = {
shiftHorizontal: 0,
shiftVertical: 0,
};
import {Animated, View} from 'react-native';
import TooltipRenderedOnPageBody from './TooltipRenderedOnPageBody';
import Hoverable from '../Hoverable';
import withWindowDimensions from '../withWindowDimensions';
import getTooltipStyles from '../../styles/getTooltipStyles';
import {propTypes, defaultProps} from './TooltipPropTypes';

class Tooltip extends PureComponent {
constructor(props) {
Expand Down Expand Up @@ -56,6 +34,7 @@ class Tooltip extends PureComponent {
this.tooltip = null;

this.isComponentMounted = false;
this.shouldStartShowAnimation = false;
this.animation = new Animated.Value(0);

this.getWrapperPosition = this.getWrapperPosition.bind(this);
Expand Down Expand Up @@ -102,9 +81,13 @@ class Tooltip extends PureComponent {
return new Promise(((resolve) => {
// Make sure the wrapper is mounted before attempting to measure it.
if (this.wrapperView) {
this.wrapperView.measureInWindow((x, y) => resolve({x, y}));
this.wrapperView.measureInWindow((x, y, width, height) => resolve({
x, y, width, height,
}));
} else {
resolve({x: 0, y: 0});
resolve({
x: 0, y: 0, width: 0, height: 0,
});
}
}));
}
Expand Down Expand Up @@ -144,16 +127,37 @@ class Tooltip extends PureComponent {
* Display the tooltip in an animation.
*/
showTooltip() {
Animated.timing(this.animation, {
toValue: 1,
duration: 140,
}).start();
this.shouldStartShowAnimation = true;

// We have to dynamically calculate the position here as tooltip could have been rendered on some elments
// that has changed its position
this.getWrapperPosition()
.then(({
x, y, width, height,
}) => {
this.setState({
wrapperWidth: width,
wrapperHeight: height,
xOffset: x,
yOffset: y,
});

// We may need this check due to the reason that the animation start will fire async
// and hideTooltip could fire before it thus keeping the Tooltip visible
if (this.shouldStartShowAnimation) {
Animated.timing(this.animation, {
toValue: 1,
duration: 140,
}).start();
}
});
}

/**
* Hide the tooltip in an animation.
*/
hideTooltip() {
this.shouldStartShowAnimation = false;
Animated.timing(this.animation, {
toValue: 0,
duration: 140,
Expand All @@ -176,34 +180,35 @@ class Tooltip extends PureComponent {
this.state.wrapperHeight,
this.state.tooltipWidth,
this.state.tooltipHeight,
this.props.shiftHorizontal,
this.props.shiftVertical,
_.result(this.props, 'shiftHorizontal'),
_.result(this.props, 'shiftVertical'),
);

return (
<Hoverable
onHoverIn={this.showTooltip}
onHoverOut={this.hideTooltip}
>
<View
ref={el => this.wrapperView = el}
onLayout={this.measureWrapperAndGetPosition}
<>
<TooltipRenderedOnPageBody
animationStyle={animationStyle}
tooltipWrapperStyle={tooltipWrapperStyle}
tooltipTextStyle={tooltipTextStyle}
pointerWrapperStyle={pointerWrapperStyle}
pointerStyle={pointerStyle}
setTooltipRef={el => this.tooltip = el}
measureTooltip={this.measureTooltip}
text={this.props.text}
/>
<Hoverable
containerStyle={this.props.containerStyle}
onHoverIn={this.showTooltip}
onHoverOut={this.hideTooltip}
>
<Animated.View style={animationStyle}>
<View
ref={el => this.tooltip = el}
onLayout={this.measureTooltip}
style={tooltipWrapperStyle}
>
<Text style={tooltipTextStyle} numberOfLines={1}>{this.props.text}</Text>
</View>
<View style={pointerWrapperStyle}>
<View style={pointerStyle} />
</View>
</Animated.View>
{this.props.children}
</View>
</Hoverable>
<View
ref={el => this.wrapperView = el}
onLayout={this.measureWrapperAndGetPosition}
style={this.props.containerStyle}
>
{this.props.children}
</View>
</Hoverable>
</>
);
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/components/Tooltip/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// We can't use the common component for the Tooltip as Web implementation uses DOM specific method to
// render the View which is not present on the Mobile.
import {propTypes, defaultProps} from './TooltipPropTypes';

/**
* There is no native support for the Hover on the Mobile platform so we just return the enclosing childrens
* @param {propTypes} props
* @returns {ReactNodeLike}
*/
const Tooltip = props => props.children;

Tooltip.propTypes = propTypes;
Tooltip.defaultProps = defaultProps;
Tooltip.displayName = 'Tooltip';
export default Tooltip;
5 changes: 5 additions & 0 deletions src/libs/OptionsListUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Str from 'expensify-common/lib/str';
import {getDefaultAvatar} from './actions/PersonalDetails';
import ONYXKEYS from '../ONYXKEYS';
import CONST from '../CONST';
import {getReportParticipantsTitle} from './reportUtils';

/**
* OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can
Expand Down Expand Up @@ -92,11 +93,14 @@ function createOption(personalDetailList, report, draftComments, activeReportID,
: '')
+ _.unescape(report.lastMessageText)
: '';
const tooltipText = getReportParticipantsTitle(lodashGet(report, ['participants'], []));

return {
text: report ? report.reportName : personalDetail.displayName,
alternateText: (showChatPreviewLine && lastMessageText) ? lastMessageText : personalDetail.login,
icons: report ? report.icons : [personalDetail.avatar],
tooltipText,
participantsList: personalDetailList,

// It doesn't make sense to provide a login in the case of a report with multiple participants since
// there isn't any one single login to refer to for a report.
Expand Down Expand Up @@ -424,4 +428,5 @@ export {
getNewGroupOptions,
getSidebarOptions,
getHeaderMessage,
getPersonalDetailsForLogins,
};
Loading

0 comments on commit 3431bf0

Please sign in to comment.