diff --git a/src/components/Hoverable/HoverablePropTypes.js b/src/components/Hoverable/HoverablePropTypes.js index 2d2842be19c5..9125e9c8669a 100644 --- a/src/components/Hoverable/HoverablePropTypes.js +++ b/src/components/Hoverable/HoverablePropTypes.js @@ -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, @@ -15,6 +19,7 @@ const propTypes = { }; const defaultProps = { + containerStyle: {}, onHoverIn: () => {}, onHoverOut: () => {}, }; diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js index da6463561b2b..c67b26c3f2de 100644 --- a/src/components/Hoverable/index.js +++ b/src/components/Hoverable/index.js @@ -57,6 +57,7 @@ class Hoverable extends Component { render() { return ( this.wrapperView = el} onMouseEnter={() => this.setIsHovered(true)} onMouseLeave={() => this.setIsHovered(false)} diff --git a/src/components/OptionsList.js b/src/components/OptionsList.js index 7f759d705788..b783cca19866 100644 --- a/src/components/OptionsList.js +++ b/src/components/OptionsList.js @@ -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 = { @@ -77,6 +80,7 @@ const defaultProps = { onSelectRow: () => {}, headerMessage: '', innerRef: null, + showTitleTooltip: false, }; class OptionsList extends Component { @@ -142,6 +146,7 @@ class OptionsList extends Component { return ( ); diff --git a/src/components/Tooltip/TooltipPropTypes.js b/src/components/Tooltip/TooltipPropTypes.js new file mode 100644 index 000000000000..699c6c643fb3 --- /dev/null +++ b/src/components/Tooltip/TooltipPropTypes.js @@ -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, +}; diff --git a/src/components/Tooltip/TooltipRenderedOnPageBody.js b/src/components/Tooltip/TooltipRenderedOnPageBody.js new file mode 100644 index 000000000000..1611147cc08b --- /dev/null +++ b/src/components/Tooltip/TooltipRenderedOnPageBody.js @@ -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( + + {props.text} + + + + , + 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); diff --git a/src/components/Tooltip.js b/src/components/Tooltip/index.js similarity index 61% rename from src/components/Tooltip.js rename to src/components/Tooltip/index.js index c72df808e47e..cd38fc3ce0fc 100644 --- a/src/components/Tooltip.js +++ b/src/components/Tooltip/index.js @@ -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) { @@ -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); @@ -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, + }); } })); } @@ -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, @@ -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 ( - - this.wrapperView = el} - onLayout={this.measureWrapperAndGetPosition} + <> + this.tooltip = el} + measureTooltip={this.measureTooltip} + text={this.props.text} + /> + - - this.tooltip = el} - onLayout={this.measureTooltip} - style={tooltipWrapperStyle} - > - {this.props.text} - - - - - - {this.props.children} - - + this.wrapperView = el} + onLayout={this.measureWrapperAndGetPosition} + style={this.props.containerStyle} + > + {this.props.children} + + + ); } } diff --git a/src/components/Tooltip/index.native.js b/src/components/Tooltip/index.native.js new file mode 100644 index 000000000000..d5092883bbad --- /dev/null +++ b/src/components/Tooltip/index.native.js @@ -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; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index f6546c64a3e7..e919f280e3db 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -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 @@ -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. @@ -424,4 +428,5 @@ export { getNewGroupOptions, getSidebarOptions, getHeaderMessage, + getPersonalDetailsForLogins, }; diff --git a/src/libs/hasEllipsis/index.js b/src/libs/hasEllipsis/index.js new file mode 100644 index 000000000000..d18af237be35 --- /dev/null +++ b/src/libs/hasEllipsis/index.js @@ -0,0 +1,11 @@ +/** + * Does an elment have ellipsis + * + * @param {HTMLElement} el Element to check + * @returns {Boolean} + */ +function hasEllipsis(el) { + return el.offsetWidth < el.scrollWidth; +} + +export default hasEllipsis; diff --git a/src/libs/hasEllipsis/index.native.js b/src/libs/hasEllipsis/index.native.js new file mode 100644 index 000000000000..2d1ec238274a --- /dev/null +++ b/src/libs/hasEllipsis/index.native.js @@ -0,0 +1 @@ +export default () => {}; diff --git a/src/libs/reportUtils.js b/src/libs/reportUtils.js new file mode 100644 index 000000000000..538076d6c183 --- /dev/null +++ b/src/libs/reportUtils.js @@ -0,0 +1,17 @@ +import _ from 'underscore'; +import Str from 'expensify-common/lib/str'; + +/** + * Returns the concatenated title for the PrimaryLogins of a report + * + * @param {Array} logins + * @returns {string} + */ +function getReportParticipantsTitle(logins) { + return _.map(logins, login => Str.removeSMSDomain(login)).join(', '); +} + +export { + // eslint-disable-next-line import/prefer-default-export + getReportParticipantsTitle, +}; diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 76a06bf34e46..08be33ce2af5 100644 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -165,6 +165,7 @@ class SearchPage extends Component { headerMessage={headerMessage} hideSectionHeaders hideAdditionalOptionStates + showTitleTooltip /> diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index bd2a0fcf4fac..8304509eb351 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -2,7 +2,7 @@ import React from 'react'; import {View, Pressable} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; -import Header from '../../components/Header'; +import lodashGet from 'lodash.get'; import styles from '../../styles/styles'; import ONYXKEYS from '../../ONYXKEYS'; import themeColors from '../../styles/themes/default'; @@ -14,6 +14,10 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/ import MultipleAvatars from '../../components/MultipleAvatars'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; +import {getReportParticipantsTitle} from '../../libs/reportUtils'; +import OptionRowTitle from './sidebar/OptionRowTitle'; +import {getPersonalDetailsForLogins} from '../../libs/OptionsListUtils'; +import {participantPropTypes} from './sidebar/optionPropTypes'; const propTypes = { // Toggles the navigationMenu open and closed @@ -25,6 +29,9 @@ const propTypes = { // Name of the report reportName: PropTypes.string, + // List of primarylogins of participants of the report + participants: PropTypes.arrayOf(PropTypes.string), + // ID of the report reportID: PropTypes.number, @@ -32,6 +39,9 @@ const propTypes = { isPinned: PropTypes.bool, }), + // Personal details of all the users + personalDetails: PropTypes.arrayOf(participantPropTypes).isRequired, + ...windowDimensionsPropTypes, }; @@ -39,51 +49,65 @@ const defaultProps = { report: null, }; -const HeaderView = props => ( - - - {props.isSmallScreenWidth && ( - - - - )} - {props.report && props.report.reportName ? ( - +const HeaderView = (props) => { + const participants = lodashGet(props.report, 'participants', []); + const reportOption = { + text: lodashGet(props.report, 'reportName', ''), + tooltipText: getReportParticipantsTitle(participants), + participantsList: getPersonalDetailsForLogins(participants, props.personalDetails), + }; + + return ( + + + {props.isSmallScreenWidth && ( { - const {participants} = props.report; - if (participants.length === 1) { - Navigation.navigate(ROUTES.getProfileRoute(participants[0])); - } - }} + onPress={props.onNavigationMenuButtonClicked} + style={[styles.LHNToggle]} > - + -
- + )} + {props.report && props.report.reportName && ( + togglePinnedState(props.report)} - style={[styles.touchableButtonImage, styles.mr0]} + onPress={() => { + if (participants.length === 1) { + Navigation.navigate(ROUTES.getProfileRoute(participants[0])); + } + }} > - + + + + + + togglePinnedState(props.report)} + style={[styles.touchableButtonImage, styles.mr0]} + > + + + - - ) : null} + )} + - -); - + ); +}; HeaderView.propTypes = propTypes; HeaderView.displayName = 'HeaderView'; HeaderView.defaultProps = defaultProps; @@ -94,5 +118,8 @@ export default compose( report: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS, + }, }), )(HeaderView); diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.js index 16abf1eaa504..a94cfebdb49c 100644 --- a/src/pages/home/report/ReportActionItemFragment.js +++ b/src/pages/home/report/ReportActionItemFragment.js @@ -7,11 +7,15 @@ import styles from '../../../styles/styles'; import themeColors from '../../../styles/themes/default'; import RenderHTML from '../../../components/RenderHTML'; import Text from '../../../components/Text'; +import Tooltip from '../../../components/Tooltip'; const propTypes = { // The message fragment needing to be displayed fragment: ReportActionFragmentPropTypes.isRequired, + // Text to be shown for tooltip When Fragment is report Actor + tooltipText: PropTypes.string, + // Is this fragment an attachment? isAttachment: PropTypes.bool, @@ -22,11 +26,12 @@ const propTypes = { const defaultProps = { isAttachment: false, loading: false, + tooltipText: '', }; class ReportActionItemFragment extends React.PureComponent { render() { - const {fragment} = this.props; + const {fragment, tooltipText} = this.props; switch (fragment.type) { case 'COMMENT': // If this is an attachment placeholder, return the placeholder component @@ -50,12 +55,14 @@ class ReportActionItemFragment extends React.PureComponent { ); case 'TEXT': return ( - - {Str.htmlDecode(fragment.text)} - + + + {Str.htmlDecode(fragment.text)} + + ); case 'LINK': return LINK; diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index ccd9ea1df280..bf40be5e8cda 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -31,6 +31,7 @@ const ReportActionItemSingle = ({action}) => { diff --git a/src/pages/home/sidebar/OptionRow.js b/src/pages/home/sidebar/OptionRow.js index 641109bc45c6..c31d128f5d8d 100644 --- a/src/pages/home/sidebar/OptionRow.js +++ b/src/pages/home/sidebar/OptionRow.js @@ -8,12 +8,13 @@ import { StyleSheet, } from 'react-native'; import styles from '../../../styles/styles'; -import optionPropTypes from './optionPropTypes'; +import {optionPropTypes} from './optionPropTypes'; import Icon from '../../../components/Icon'; import {Pencil, PinCircle, Checkmark} from '../../../components/Icon/Expensicons'; import MultipleAvatars from '../../../components/MultipleAvatars'; import themeColors from '../../../styles/themes/default'; import Hoverable from '../../../components/Hoverable'; +import OptionRowTitle from './OptionRowTitle'; const propTypes = { // Style for hovered state @@ -40,6 +41,9 @@ const propTypes = { // Force the text style to be the unread style forceTextUnreadStyle: PropTypes.bool, + + // Whether to show the title tooltip + showTitleTooltip: PropTypes.bool, }; const defaultProps = { @@ -48,6 +52,7 @@ const defaultProps = { showSelectedState: false, isSelected: false, forceTextUnreadStyle: false, + showTitleTooltip: false, }; const OptionRow = ({ @@ -59,6 +64,7 @@ const OptionRow = ({ showSelectedState, isSelected, forceTextUnreadStyle, + showTitleTooltip, }) => { const textStyle = optionIsFocused ? styles.sidebarLinkActiveText @@ -105,9 +111,13 @@ const OptionRow = ({ ) } - - {option.text} - + + {option.alternateText ? ( containerRight ? -(tooltipX - newToolX) : 0; + } + + + render() { + const { + option, style, tooltipEnabled, numberOfLines, + } = this.props; + + if (!tooltipEnabled) { + return {option.text}; + } + return ( + + // Tokenization of string only support 1 numberofLines on Web + this.containerRef = el} + > + {_.map(option.participantsList, (participant, index) => ( + + 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 */} + this.childRefs[index] = el}> + {participant.displayName} + + + {index < option.participantsList.length - 1 && } + + ))} + {option.participantsList.length > 1 && this.state.isEllipsisActive + && ( + + + {/* There is some Gap for real ellipsis so we are adding 4 `.` to cover */} + .... + + + )} + + ); + } +} +OptionRowTitle.propTypes = propTypes; +OptionRowTitle.defaultProps = defaultProps; +OptionRowTitle.displayName = 'OptionRowTitle'; + +export default OptionRowTitle; diff --git a/src/pages/home/sidebar/OptionRowTitle/index.native.js b/src/pages/home/sidebar/OptionRowTitle/index.native.js new file mode 100644 index 000000000000..b863a115579a --- /dev/null +++ b/src/pages/home/sidebar/OptionRowTitle/index.native.js @@ -0,0 +1,20 @@ +// As we don't have to show tooltips of the Native platform so we simply render the option title which wraps. +import React from 'react'; +import {Text} from 'react-native'; +import {propTypes, defaultProps} from './OptionRowTitleProps'; + +const OptionRowTitle = ({ + style, + option, + numberOfLines, +}) => ( + + {option.text} + +); + +OptionRowTitle.propTypes = propTypes; +OptionRowTitle.defaultProps = defaultProps; +OptionRowTitle.displayName = 'OptionRowTitle'; + +export default OptionRowTitle; diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index c4215b28e658..2f64b227b13e 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -18,6 +18,7 @@ import {getSidebarOptions} from '../../../libs/OptionsListUtils'; import {getDefaultAvatar} from '../../../libs/actions/PersonalDetails'; import KeyboardSpacer from '../../../components/KeyboardSpacer'; import CONST from '../../../CONST'; +import {participantPropTypes} from './optionPropTypes'; const propTypes = { // Toggles the navigation menu open and closed @@ -41,11 +42,7 @@ const propTypes = { draftComments: PropTypes.objectOf(PropTypes.string), // List of users' personal details - personalDetails: PropTypes.objectOf(PropTypes.shape({ - login: PropTypes.string.isRequired, - avatar: PropTypes.string.isRequired, - displayName: PropTypes.string.isRequired, - })), + personalDetails: PropTypes.objectOf(participantPropTypes), // The personal details of the person who is logged in myPersonalDetails: PropTypes.shape({ @@ -149,6 +146,7 @@ class SidebarLinks extends React.Component { this.props.onLinkClick(); }} hideSectionHeaders + showTitleTooltip disableFocusOptions={this.props.isSmallScreenWidth} /> diff --git a/src/pages/home/sidebar/optionPropTypes.js b/src/pages/home/sidebar/optionPropTypes.js index 2d1d8d999dc1..ec2881f790b5 100644 --- a/src/pages/home/sidebar/optionPropTypes.js +++ b/src/pages/home/sidebar/optionPropTypes.js @@ -1,12 +1,26 @@ import PropTypes from 'prop-types'; -export default PropTypes.shape({ +const participantPropTypes = PropTypes.shape({ + // Primary login of participant + login: PropTypes.string, + + // Display Name of participant + displayName: PropTypes.string, + + // Avatar url of participant + avatar: PropTypes.string, +}); + +const optionPropTypes = PropTypes.shape({ // The full name of the user if available, otherwise the login (email/phone number) of the user text: PropTypes.string.isRequired, // Subtitle to show under report displayName, mostly lastMessageText of the report alternateText: PropTypes.string.isRequired, + // List of participants of the report + participantsList: PropTypes.arrayOf(participantPropTypes).isRequired, + // The array URLs of the person's avatar icon: PropTypes.arrayOf(PropTypes.string), @@ -15,4 +29,12 @@ export default PropTypes.shape({ // The option key provided to FlatList keyExtractor keyForList: PropTypes.string, + + // Text to show for tooltip + tooltipText: PropTypes.string, }); + +export { + participantPropTypes, + optionPropTypes, +}; diff --git a/src/styles/getTooltipStyles.js b/src/styles/getTooltipStyles.js index 29b6c3c02c97..21699e5428d1 100644 --- a/src/styles/getTooltipStyles.js +++ b/src/styles/getTooltipStyles.js @@ -111,19 +111,24 @@ export default function getTooltipStyles( return { animationStyle: { + // remember Transform causes a new Local cordinate system + // https://drafts.csswg.org/css-transforms-1/#transform-rendering + // so Position fixed children will be relative to this new Local cordinate system transform: [{ scale: currentSize, }], }, tooltipWrapperStyle: { - position: 'absolute', + position: 'fixed', backgroundColor: themeColors.heading, borderRadius: variables.componentBorderRadiusSmall, ...tooltipVerticalPadding, ...spacing.ph2, + zIndex: variables.tooltipzIndex, - // Because it uses absolute positioning, the top-left corner of the tooltip is aligned - // with the top-left corner of the wrapped component by default. + // Because it uses fixed positioning, the top-left corner of the tooltip is aligned + // with the top-left corner of the window by default. + // we will use yOffset to position the tooltip relative to the Wrapped Component // So we need to shift the tooltip vertically and horizontally to position it correctly. // // First, we'll position it vertically. @@ -132,12 +137,13 @@ export default function getTooltipStyles( top: shouldShowBelow // We need to shift the tooltip down below the component. So shift the tooltip down (+) by... - ? componentHeight + POINTER_HEIGHT + manualShiftVertical + ? yOffset + componentHeight + POINTER_HEIGHT + manualShiftVertical // We need to shift the tooltip up above the component. So shift the tooltip up (-) by... - : -(tooltipHeight + POINTER_HEIGHT) + manualShiftVertical, + : (yOffset - (tooltipHeight + POINTER_HEIGHT)) + manualShiftVertical, // Next, we'll position it horizontally. + // we will use xOffset to position the tooltip relative to the Wrapped Component // To shift the tooltip right, we'll give `left` a positive value. // To shift the tooltip left, we'll give `left` a negative value. // @@ -148,7 +154,7 @@ export default function getTooltipStyles( // so the tooltip's center lines up with the center of the wrapped component. // 3) Add the horizontal shift (left or right) computed above to keep it out of the gutters. // 4) Lastly, add the manual horizontal shift passed in as a parameter. - left: ((componentWidth / 2) - (tooltipWidth / 2)) + horizontalShift + manualShiftHorizontal, + left: xOffset + ((componentWidth / 2) - (tooltipWidth / 2)) + horizontalShift + manualShiftHorizontal, }, tooltipTextStyle: { color: themeColors.textReversed, @@ -156,14 +162,13 @@ export default function getTooltipStyles( fontSize: tooltipFontSize, }, pointerWrapperStyle: { - position: 'absolute', + position: 'fixed', - - // By default, the pointer's top-left will align with the top-left of the wrapped component. + // By default, the pointer's top-left will align with the top-left of the wrapped tooltip. // // To align it vertically, we'll: // - // Shift the pointer up (-) by its height, so that the bottom of the pointer lines up + // Shift the pointer up (-) by component's height, so that the bottom of the pointer lines up // with the top of the wrapped component. // // OR if it should show below: @@ -172,14 +177,16 @@ export default function getTooltipStyles( // so that the top of the pointer aligns with the bottom of the component. // // Always add the manual vertical shift passed in as a parameter. - top: shouldShowBelow ? componentHeight + manualShiftVertical : manualShiftVertical - POINTER_HEIGHT, + top: shouldShowBelow ? manualShiftVertical - POINTER_HEIGHT : tooltipHeight + manualShiftVertical, // To align it horizontally, we'll: - // 1) Shift the pointer to the right (+) by the half the component's width, - // so the left edge of the pointer lines up with the component's center. + // 1) Shift the pointer to the right (+) by the half the tooltipWidth's width, + // so the left edge of the pointer lines up with the tooltipWidth's center. // 2) To the left (-) by half the pointer's width, - // so the pointer's center lines up with the component's center. - left: (componentWidth / 2) - (POINTER_WIDTH / 2), + // so the pointer's center lines up with the tooltipWidth's center. + // 3) Due to the tip start from the left edge of wrapper Tooltip so we have to remove the + // horizontalShift which is added to adjust it into the Window + left: -horizontalShift + ((tooltipWidth / 2) - (POINTER_WIDTH / 2)), }, pointerStyle: { width: 0, diff --git a/src/styles/styles.js b/src/styles/styles.js index 1f44256c7858..955fd6a852cc 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -559,6 +559,17 @@ const styles = { ...whiteSpace.noWrap, }, + optionDisplayNameTooltipWrapper: { + position: 'relative', + }, + + optionDisplayNameTooltipEllipsis: { + position: 'absolute', + opacity: 0, + right: 0, + bottom: 0, + }, + optionAlternateText: { color: themeColors.textSupporting, fontFamily: fontFamily.GTA, diff --git a/src/styles/utilities/display.js b/src/styles/utilities/display.js index 275ac345883e..a16a62694af8 100644 --- a/src/styles/utilities/display.js +++ b/src/styles/utilities/display.js @@ -16,4 +16,8 @@ export default { dNone: { display: 'none', }, + + dInline: { + display: 'inline', + }, }; diff --git a/src/styles/variables.js b/src/styles/variables.js index d4b5ecc592a3..6463935aa836 100644 --- a/src/styles/variables.js +++ b/src/styles/variables.js @@ -19,4 +19,5 @@ export default { safeInsertPercentage: 0.7, sideBarWidth: 375, pdfPageMaxWidth: 992, + tooltipzIndex: 10050, };