Skip to content

Commit

Permalink
Merge pull request #20953 from Expensify/srikar-refactorDisplayNames
Browse files Browse the repository at this point in the history
Change Display Name to Functional Component
  • Loading branch information
PauloGasparSv authored Aug 17, 2023
2 parents 15c49f5 + 040cf96 commit 63d87ba
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 109 deletions.
79 changes: 79 additions & 0 deletions src/components/DisplayNames/DisplayNamesTooltipItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import PropTypes from 'prop-types';
import React, {useCallback} from 'react';
import styles from '../../styles/styles';
import Text from '../Text';
import UserDetailsTooltip from '../UserDetailsTooltip';

const propTypes = {
index: PropTypes.number,

/** The full title of the DisplayNames component (not split up) */
getTooltipShiftX: PropTypes.func,

/** The Account ID for the tooltip */
accountID: PropTypes.number,

/** The name to display in bold */
displayName: PropTypes.string,

/** The login for the tooltip fallback */
login: PropTypes.string,

/** The avatar for the tooltip fallback */
avatar: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),

/** Arbitrary styles of the displayName text */
// eslint-disable-next-line react/forbid-prop-types
textStyles: PropTypes.arrayOf(PropTypes.object),

/** Refs to all the names which will be used to correct the horizontal position of the tooltip */
childRefs: PropTypes.shape({
// eslint-disable-next-line react/forbid-prop-types
current: PropTypes.arrayOf(PropTypes.object),
}),
};

const defaultProps = {
index: 0,
getTooltipShiftX: () => {},
accountID: 0,
displayName: '',
login: '',
avatar: '',
textStyles: [],
childRefs: {current: []},
};

function DisplayNamesTooltipItem({index, getTooltipShiftX, accountID, avatar, login, displayName, textStyles, childRefs}) {
const tooltipIndexBridge = useCallback(() => getTooltipShiftX(index), [getTooltipShiftX, index]);

return (
<UserDetailsTooltip
key={index}
accountID={accountID}
fallbackUserDetails={{
avatar,
login,
displayName,
}}
shiftHorizontal={tooltipIndexBridge}
>
{/* We need to get the refs to all the names which will be used to correct the horizontal position of the tooltip */}
<Text
eslint-disable-next-line
no-param-reassign
// eslint-disable-next-line no-param-reassign
ref={(el) => (childRefs.current[index] = el)}
style={[...textStyles, styles.pre]}
>
{displayName}
</Text>
</UserDetailsTooltip>
);
}

DisplayNamesTooltipItem.propTypes = propTypes;
DisplayNamesTooltipItem.defaultProps = defaultProps;
DisplayNamesTooltipItem.displayName = 'DisplayNamesTooltipItem';

export default DisplayNamesTooltipItem;
92 changes: 92 additions & 0 deletions src/components/DisplayNames/DisplayNamesWithTooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, {Fragment, useCallback, useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import _ from 'underscore';
import styles from '../../styles/styles';
import Text from '../Text';
import Tooltip from '../Tooltip';
import DisplayNamesTooltipItem from './DisplayNamesTooltipItem';
import {defaultProps, propTypes} from './displayNamesPropTypes';

function DisplayNamesWithToolTip(props) {
const containerRef = useRef(null);
const childRefs = useRef([]);
const [isEllipsisActive, setIsEllipsisActive] = useState(false);

useEffect(() => {
setIsEllipsisActive(
containerRef.current && containerRef.current.offsetWidth && containerRef.current.scrollWidth && containerRef.current.offsetWidth < containerRef.current.scrollWidth,
);
}, []);

/**
* We may need to shift the Tooltip horizontally as some of the inline text wraps well with ellipsis,
* but their container node overflows the parent view which causes the tooltip to be misplaced.
*
* So we shift it by calculating it as follows:
* 1. We get the container layout and take the Child inline text node.
* 2. Now we get the tooltip original position.
* 3. If inline node's right edge is overflowing the container's right edge, we set the tooltip to the center
* of the distance between the left edge of the inline node and right edge of the container.
* @param {Number} index Used to get the Ref to the node at the current index
* @returns {Number} Distance to shift the tooltip horizontally
*/
const getTooltipShiftX = useCallback((index) => {
// Only shift the tooltip in case the containerLayout or Refs to the text node are available
if (!containerRef || !childRefs.current[index]) {
return;
}
const {width: containerWidth, left: containerLeft} = containerRef.current.getBoundingClientRect();

// We have to return the value as Number so we can't use `measureWindow` which takes a callback
const {width: textNodeWidth, left: textNodeLeft} = childRefs.current[index].getBoundingClientRect();
const tooltipX = textNodeWidth / 2 + textNodeLeft;
const containerRight = containerWidth + containerLeft;
const textNodeRight = textNodeWidth + textNodeLeft;
const newToolX = textNodeLeft + (containerRight - textNodeLeft) / 2;

// When text right end is beyond the Container right end
return textNodeRight > containerRight ? -(tooltipX - newToolX) : 0;
}, []);

return (
// Tokenization of string only support prop numberOfLines on Web
<Text
style={[...props.textStyles, styles.pRelative]}
numberOfLines={props.numberOfLines || undefined}
ref={(el) => (containerRef.current = el)}
>
{props.shouldUseFullTitle
? props.fullTitle
: _.map(props.displayNamesWithTooltips, ({displayName, accountID, avatar, login}, index) => (
<Fragment key={index}>
<DisplayNamesTooltipItem
index={index}
getTooltipShiftX={getTooltipShiftX}
accountID={accountID}
displayName={displayName}
login={login}
avatar={avatar}
textStyles={props.textStyles}
childRefs={childRefs}
addComma={index < props.displayNamesWithTooltips.length - 1}
/>
{index < props.displayNamesWithTooltips.length - 1 && <Text style={props.textStyles}>,&nbsp;</Text>}
</Fragment>
))}
{Boolean(isEllipsisActive) && (
<View style={styles.displayNameTooltipEllipsis}>
<Tooltip text={props.fullTitle}>
{/* There is some Gap for real ellipsis so we are adding 4 `.` to cover */}
<Text>....</Text>
</Tooltip>
</View>
)}
</Text>
);
}

DisplayNamesWithToolTip.propTypes = propTypes;
DisplayNamesWithToolTip.defaultProps = defaultProps;
DisplayNamesWithToolTip.displayName = 'DisplayNamesWithTooltip';

export default DisplayNamesWithToolTip;
39 changes: 39 additions & 0 deletions src/components/DisplayNames/DisplayNamesWithoutTooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from '../../styles/styles';
import Text from '../Text';

const propTypes = {
/** The full title of the DisplayNames component (not split up) */
fullTitle: PropTypes.string,

/** Arbitrary styles of the displayName text */
// eslint-disable-next-line react/forbid-prop-types
textStyles: PropTypes.arrayOf(PropTypes.object),

/** Number of lines before wrapping */
numberOfLines: PropTypes.number,
};

const defaultProps = {
fullTitle: '',
textStyles: [],
numberOfLines: 1,
};

function DisplayNamesWithoutTooltip({textStyles, numberOfLines, fullTitle}) {
return (
<Text
style={[...textStyles, numberOfLines === 1 ? styles.pre : styles.preWrap]}
numberOfLines={numberOfLines}
>
{fullTitle}
</Text>
);
}

DisplayNamesWithoutTooltip.propTypes = propTypes;
DisplayNamesWithoutTooltip.defaultProps = defaultProps;
DisplayNamesWithoutTooltip.displayName = 'DisplayNamesWithoutTooltip';

export default DisplayNamesWithoutTooltip;
125 changes: 16 additions & 109 deletions src/components/DisplayNames/index.js
Original file line number Diff line number Diff line change
@@ -1,118 +1,25 @@
import _ from 'underscore';
import React, {Fragment, PureComponent} from 'react';
import {View} from 'react-native';
import {propTypes, defaultProps} from './displayNamesPropTypes';
import styles from '../../styles/styles';
import Tooltip from '../Tooltip';
import Text from '../Text';
import UserDetailsTooltip from '../UserDetailsTooltip';

class DisplayNames extends PureComponent {
constructor(props) {
super(props);
this.containerRef = null;
this.childRefs = [];
this.state = {
isEllipsisActive: false,
};
this.getTooltipShiftX = this.getTooltipShiftX.bind(this);
}

componentDidMount() {
this.setState({
isEllipsisActive: this.containerRef && this.containerRef.offsetWidth && this.containerRef.scrollWidth && this.containerRef.offsetWidth < this.containerRef.scrollWidth,
});
}

/**
* We may need to shift the Tooltip horizontally as some of the inline text wraps well with ellipsis,
* but their container node overflows the parent view which causes the tooltip to be misplaced.
*
* So we shift it by calculating it as follows:
* 1. We get the container layout and take the Child inline text node.
* 2. Now we get the tooltip original position.
* 3. If inline node's right edge is overflowing the container's right edge, we set the tooltip to the center
* of the distance between the left edge of the inline node and right edge of the container.
* @param {Number} index Used to get the Ref to the node at the current index
* @returns {Number} Distance to shift the tooltip horizontally
*/
getTooltipShiftX(index) {
// Only shift the tooltip in case the container ref or Refs to the text node are available
if (!this.containerRef || !this.childRefs[index]) {
return;
}
const {width: containerWidth, left: containerLeft} = this.containerRef.getBoundingClientRect();

// We have to return the value as Number so we can't use `measureWindow` which takes a callback
const {width: textNodeWidth, left: textNodeLeft} = this.childRefs[index].getBoundingClientRect();
const tooltipX = textNodeWidth / 2 + textNodeLeft;
const containerRight = containerWidth + containerLeft;
const textNodeRight = textNodeWidth + textNodeLeft;
const newToolX = textNodeLeft + (containerRight - textNodeLeft) / 2;

// When text right end is beyond the Container right end
return textNodeRight > containerRight ? -(tooltipX - newToolX) : 0;
}

render() {
if (!this.props.tooltipEnabled) {
// No need for any complex text-splitting, just return a simple Text component
return (
<Text
style={[...this.props.textStyles, this.props.numberOfLines === 1 ? styles.pre : styles.preWrap]}
numberOfLines={this.props.numberOfLines}
>
{this.props.fullTitle}
</Text>
);
}
import React from 'react';
import DisplayNamesWithToolTip from './DisplayNamesWithTooltip';
import DisplayNamesWithoutTooltip from './DisplayNamesWithoutTooltip';
import {defaultProps, propTypes} from './displayNamesPropTypes';

function DisplayNames(props) {
if (!props.tooltipEnabled) {
return (
// Tokenization of string only support prop numberOfLines on Web
<Text
style={[...this.props.textStyles, styles.pRelative]}
numberOfLines={this.props.numberOfLines || undefined}
ref={(el) => (this.containerRef = el)}
>
{this.props.shouldUseFullTitle
? this.props.fullTitle
: _.map(this.props.displayNamesWithTooltips, ({displayName, accountID, avatar, login}, index) => (
<Fragment key={index}>
<UserDetailsTooltip
key={index}
accountID={accountID}
fallbackUserDetails={{
avatar,
login,
displayName,
}}
shiftHorizontal={() => this.getTooltipShiftX(index)}
>
{/* // We need to get the refs to all the names which will be used to correct
the horizontal position of the tooltip */}
<Text
ref={(el) => (this.childRefs[index] = el)}
style={[...this.props.textStyles, styles.pre]}
>
{displayName}
</Text>
</UserDetailsTooltip>
{index < this.props.displayNamesWithTooltips.length - 1 && <Text style={this.props.textStyles}>,&nbsp;</Text>}
</Fragment>
))}
{Boolean(this.state.isEllipsisActive) && (
<View style={styles.displayNameTooltipEllipsis}>
<Tooltip text={this.props.fullTitle}>
{/* There is some Gap for real ellipsis so we are adding 4 `.` to cover */}
<Text>....</Text>
</Tooltip>
</View>
)}
</Text>
<DisplayNamesWithoutTooltip
textStyles={props.textStyles}
numberOfLines={props.numberOfLines}
fullTitle={props.fullTitle}
/>
);
}

// eslint-disable-next-line react/jsx-props-no-spreading
return <DisplayNamesWithToolTip {...props} />;
}

DisplayNames.propTypes = propTypes;
DisplayNames.defaultProps = defaultProps;
DisplayNames.displayName = 'DisplayNames';

export default DisplayNames;

0 comments on commit 63d87ba

Please sign in to comment.