diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js index 7a83be449..edbd30807 100644 --- a/web/chat/composed-message.react.js +++ b/web/chat/composed-message.react.js @@ -1,226 +1,226 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { Circle as CircleIcon, CheckCircle as CheckCircleIcon, XCircle as XCircleIcon, } from 'react-feather'; import { type ChatMessageInfoItem } from 'lib/selectors/chat-selectors'; import { useSidebarExistsOrCanBeCreated } from 'lib/shared/thread-utils'; import { stringForUser } from 'lib/shared/user-utils'; import { assertComposableMessageType } from 'lib/types/message-types'; import { type ThreadInfo } from 'lib/types/thread-types'; import { type InputState, InputStateContext } from '../input/input-state'; import css from './chat-message-list.css'; import FailedSend from './failed-send.react'; import { InlineSidebar } from './inline-sidebar.react'; import MessageTooltip from './message-tooltip.react'; import { type OnMessagePositionWithContainerInfo, type MessagePositionInfo, } from './position-types'; import { tooltipPositions } from './tooltip-utils'; const availableTooltipPositionsForViewerMessage = [ - tooltipPositions.TOP_RIGHT, + tooltipPositions.RIGHT_TOP, tooltipPositions.LEFT, ]; const availableTooltipPositionsForNonViewerMessage = [ - tooltipPositions.TOP_LEFT, + tooltipPositions.LEFT_TOP, tooltipPositions.RIGHT, ]; type BaseProps = { +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, +sendFailed: boolean, +setMouseOverMessagePosition: ( messagePositionInfo: MessagePositionInfo, ) => void, +mouseOverMessagePosition?: ?OnMessagePositionWithContainerInfo, +canReply: boolean, +children: React.Node, +fixedWidth?: boolean, +borderRadius: number, }; type BaseConfig = React.Config; type Props = { ...BaseProps, // Redux state +sidebarExistsOrCanBeCreated: boolean, // withInputState +inputState: ?InputState, }; class ComposedMessage extends React.PureComponent { static defaultProps: { +borderRadius: number } = { borderRadius: 8, }; render(): React.Node { assertComposableMessageType(this.props.item.messageInfo.type); const { borderRadius, item, threadInfo } = this.props; const { id, creator } = item.messageInfo; const threadColor = threadInfo.color; const { isViewer } = creator; const contentClassName = classNames({ [css.content]: true, [css.viewerContent]: isViewer, [css.nonViewerContent]: !isViewer, }); const messageBoxContainerClassName = classNames({ [css.messageBoxContainer]: true, [css.fixedWidthMessageBoxContainer]: this.props.fixedWidth, }); const messageBoxClassName = classNames({ [css.messageBox]: true, [css.fixedWidthMessageBox]: this.props.fixedWidth, }); const messageBoxStyle = { borderTopRightRadius: isViewer && !item.startsCluster ? 0 : borderRadius, borderBottomRightRadius: isViewer && !item.endsCluster ? 0 : borderRadius, borderTopLeftRadius: !isViewer && !item.startsCluster ? 0 : borderRadius, borderBottomLeftRadius: !isViewer && !item.endsCluster ? 0 : borderRadius, }; let authorName = null; if (!isViewer && item.startsCluster) { authorName = ( {stringForUser(creator)} ); } let deliveryIcon = null; let failedSendInfo = null; if (isViewer) { let deliveryIconSpan; let deliveryIconColor = threadColor; if (id !== null && id !== undefined) { deliveryIconSpan = ; } else if (this.props.sendFailed) { deliveryIconSpan = ; deliveryIconColor = 'FF0000'; failedSendInfo = ; } else { deliveryIconSpan = ; } deliveryIcon = (
{deliveryIconSpan}
); } let messageTooltip; if ( this.props.mouseOverMessagePosition && this.props.mouseOverMessagePosition.item.messageInfo.id === id && (this.props.sidebarExistsOrCanBeCreated || this.props.canReply) ) { // eslint-disable-next-line no-unused-vars const availableTooltipPositions = isViewer ? availableTooltipPositionsForViewerMessage : availableTooltipPositionsForNonViewerMessage; messageTooltip = ; } let messageTooltipLinks; if (messageTooltip) { const tooltipLinksClassName = classNames({ [css.messageTooltipActiveArea]: true, [css.viewerMessageTooltipActiveArea]: isViewer, [css.nonViewerMessageActiveArea]: !isViewer, }); messageTooltipLinks = (
{messageTooltip}
); } const viewerTooltipLinks = isViewer ? messageTooltipLinks : null; const nonViewerTooltipLinks = !isViewer ? messageTooltipLinks : null; let inlineSidebar = null; if (item.threadCreatedFromMessage) { const positioning = isViewer ? 'right' : 'left'; inlineSidebar = (
); } return ( {authorName}
{viewerTooltipLinks}
{this.props.children}
{nonViewerTooltipLinks}
{deliveryIcon}
{failedSendInfo} {inlineSidebar}
); } onMouseEnter: (event: SyntheticEvent) => void = event => { const { item } = this.props; const rect = event.currentTarget.getBoundingClientRect(); const { top, bottom, left, right, height, width } = rect; const messagePosition = { top, bottom, left, right, height, width }; this.props.setMouseOverMessagePosition({ type: 'on', item, messagePosition, }); }; onMouseLeave: () => void = () => { const { item } = this.props; this.props.setMouseOverMessagePosition({ type: 'off', item }); }; } type ConnectedConfig = React.Config< BaseProps, typeof ComposedMessage.defaultProps, >; const ConnectedComposedMessage: React.ComponentType = React.memo( function ConnectedComposedMessage(props) { const sidebarExistsOrCanBeCreated = useSidebarExistsOrCanBeCreated( props.threadInfo, props.item, ); const inputState = React.useContext(InputStateContext); return ( ); }, ); export default ConnectedComposedMessage; diff --git a/web/chat/message-timestamp-tooltip.react.js b/web/chat/message-timestamp-tooltip.react.js index 68bb9584a..24a741892 100644 --- a/web/chat/message-timestamp-tooltip.react.js +++ b/web/chat/message-timestamp-tooltip.react.js @@ -1,148 +1,148 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { isComposableMessageType } from 'lib/types/message-types'; import { longAbsoluteDate } from 'lib/utils/date-utils'; import css from './message-timestamp-tooltip.css'; import type { OnMessagePositionWithContainerInfo } from './position-types'; import { type TooltipPosition, tooltipPositions, sizeOfTooltipArrow, } from './tooltip-utils'; import { TooltipMenu, type TooltipStyle, TooltipTextItem, } from './tooltip.react'; const availablePositionsForComposedViewerMessage = [tooltipPositions.LEFT]; const availablePositionsForNonComposedOrNonViewerMessage = [ - tooltipPositions.BOTTOM_RIGHT, + tooltipPositions.RIGHT_BOTTOM, ]; type Props = { +messagePositionInfo: OnMessagePositionWithContainerInfo, +timeZone: ?string, }; function MessageTimestampTooltip(props: Props): React.Node { const { messagePositionInfo, timeZone } = props; const { time, creator, type } = messagePositionInfo.item.messageInfo; const text = React.useMemo(() => longAbsoluteDate(time, timeZone), [ time, timeZone, ]); const availableTooltipPositions = React.useMemo(() => { const { isViewer } = creator; const isComposed = isComposableMessageType(type); return isComposed && isViewer ? availablePositionsForComposedViewerMessage : availablePositionsForNonComposedOrNonViewerMessage; }, [creator, type]); const { messagePosition, containerPosition } = messagePositionInfo; const pointingToInfo = React.useMemo(() => { return { containerPosition, itemPosition: messagePosition, }; }, [messagePosition, containerPosition]); const getTooltipStyle = React.useCallback( (tooltipPosition: TooltipPosition) => getTimestampTooltipStyle(messagePositionInfo, tooltipPosition), [messagePositionInfo], ); return ( ); } function getTimestampTooltipStyle( messagePositionInfo: OnMessagePositionWithContainerInfo, tooltipPosition: TooltipPosition, ): TooltipStyle { const { messagePosition, containerPosition } = messagePositionInfo; const { height: containerHeight, width: containerWidth } = containerPosition; let style, className; if (tooltipPosition === tooltipPositions.LEFT) { const centerOfMessage = messagePosition.top + messagePosition.height / 2; const tooltipPointing = Math.max( Math.min(centerOfMessage, containerHeight), 0, ); style = { right: containerWidth - messagePosition.left + sizeOfTooltipArrow + 2, bottom: containerHeight - tooltipPointing - 5 * sizeOfTooltipArrow, }; className = css.messageLeftTooltip; } else if (tooltipPosition === tooltipPositions.RIGHT) { const centerOfMessage = messagePosition.top + messagePosition.height / 2; const tooltipPointing = Math.max( Math.min(centerOfMessage, containerHeight), 0, ); style = { left: messagePosition.right + sizeOfTooltipArrow, top: tooltipPointing, }; className = css.messageRightTooltip; - } else if (tooltipPosition === tooltipPositions.TOP_LEFT) { + } else if (tooltipPosition === tooltipPositions.LEFT_TOP) { const tooltipPointing = Math.min( containerHeight - messagePosition.top, containerHeight, ); style = { left: messagePosition.left, bottom: tooltipPointing + sizeOfTooltipArrow, }; className = css.messageTopLeftTooltip; - } else if (tooltipPosition === tooltipPositions.TOP_RIGHT) { + } else if (tooltipPosition === tooltipPositions.RIGHT_TOP) { const tooltipPointing = Math.min( containerHeight - messagePosition.top, containerHeight, ); style = { right: containerWidth - messagePosition.right, bottom: tooltipPointing + sizeOfTooltipArrow, }; className = css.messageTopRightTooltip; - } else if (tooltipPosition === tooltipPositions.BOTTOM_LEFT) { + } else if (tooltipPosition === tooltipPositions.LEFT_BOTTOM) { const tooltipPointing = Math.min(messagePosition.bottom, containerHeight); style = { left: messagePosition.left, top: tooltipPointing + sizeOfTooltipArrow, }; className = css.messageBottomLeftTooltip; - } else if (tooltipPosition === tooltipPositions.BOTTOM_RIGHT) { + } else if (tooltipPosition === tooltipPositions.RIGHT_BOTTOM) { const centerOfMessage = messagePosition.top + messagePosition.height / 2; const tooltipPointing = Math.max( Math.min(centerOfMessage, containerHeight), 0, ); style = { left: messagePosition.right + sizeOfTooltipArrow + 2, bottom: containerHeight - tooltipPointing - 5 * sizeOfTooltipArrow, }; className = css.messageBottomRightTooltip; } invariant( className && style, `${tooltipPosition} is not valid for timestamp tooltip`, ); return { className, style }; } export default MessageTimestampTooltip; diff --git a/web/chat/robotext-message.react.js b/web/chat/robotext-message.react.js index fdbc0894b..e663cc244 100644 --- a/web/chat/robotext-message.react.js +++ b/web/chat/robotext-message.react.js @@ -1,214 +1,214 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { type RobotextChatMessageInfoItem } from 'lib/selectors/chat-selectors'; import { threadInfoSelector } from 'lib/selectors/thread-selectors'; import { splitRobotext, parseRobotextEntity } from 'lib/shared/message-utils'; import { useSidebarExistsOrCanBeCreated } from 'lib/shared/thread-utils'; import type { Dispatch } from 'lib/types/redux-types'; import { type ThreadInfo } from 'lib/types/thread-types'; import Markdown from '../markdown/markdown.react'; import { linkRules } from '../markdown/rules.react'; import { updateNavInfoActionType } from '../redux/action-types'; import { useSelector } from '../redux/redux-utils'; import { InlineSidebar } from './inline-sidebar.react'; import MessageTooltip from './message-tooltip.react'; import type { MessagePositionInfo, OnMessagePositionWithContainerInfo, } from './position-types'; import css from './robotext-message.css'; import { tooltipPositions } from './tooltip-utils'; // eslint-disable-next-line no-unused-vars const availableTooltipPositionsForRobotext = [ - tooltipPositions.TOP_RIGHT, + tooltipPositions.RIGHT_TOP, tooltipPositions.RIGHT, tooltipPositions.LEFT, ]; type BaseProps = { +item: RobotextChatMessageInfoItem, +threadInfo: ThreadInfo, +setMouseOverMessagePosition: ( messagePositionInfo: MessagePositionInfo, ) => void, +mouseOverMessagePosition: ?OnMessagePositionWithContainerInfo, }; type Props = { ...BaseProps, // Redux state +sidebarExistsOrCanBeCreated: boolean, }; class RobotextMessage extends React.PureComponent { render() { let inlineSidebar; if (this.props.item.threadCreatedFromMessage) { inlineSidebar = (
); } const { item, sidebarExistsOrCanBeCreated } = this.props; const { id } = item.messageInfo; let messageTooltip; if ( this.props.mouseOverMessagePosition && this.props.mouseOverMessagePosition.item.messageInfo.id === id && sidebarExistsOrCanBeCreated ) { messageTooltip = ; } let messageTooltipLinks; if (messageTooltip) { messageTooltipLinks = (
{messageTooltip}
); } return (
{this.linkedRobotext()} {messageTooltipLinks}
{inlineSidebar}
); } linkedRobotext() { const { item } = this.props; const { robotext } = item; const robotextParts = splitRobotext(robotext); const textParts = []; let keyIndex = 0; for (const splitPart of robotextParts) { if (splitPart === '') { continue; } if (splitPart.charAt(0) !== '<') { const key = `text${keyIndex++}`; textParts.push( {decodeURI(splitPart)} , ); continue; } const { rawText, entityType, id } = parseRobotextEntity(splitPart); if (entityType === 't' && id !== item.messageInfo.threadID) { textParts.push(); } else if (entityType === 'c') { textParts.push(); } else { textParts.push(rawText); } } return textParts; } onMouseEnter = (event: SyntheticEvent) => { const { item } = this.props; const rect = event.currentTarget.getBoundingClientRect(); const { top, bottom, left, right, height, width } = rect; const messagePosition = { top, bottom, left, right, height, width }; this.props.setMouseOverMessagePosition({ type: 'on', item, messagePosition, }); }; onMouseLeave = () => { const { item } = this.props; this.props.setMouseOverMessagePosition({ type: 'off', item }); }; } type BaseInnerThreadEntityProps = { +id: string, +name: string, }; type InnerThreadEntityProps = { ...BaseInnerThreadEntityProps, +threadInfo: ThreadInfo, +dispatch: Dispatch, }; class InnerThreadEntity extends React.PureComponent { render() { return {this.props.name}; } onClickThread = (event: SyntheticEvent) => { event.preventDefault(); const id = this.props.id; this.props.dispatch({ type: updateNavInfoActionType, payload: { activeChatThreadID: id, }, }); }; } const ThreadEntity = React.memo( function ConnectedInnerThreadEntity(props: BaseInnerThreadEntityProps) { const { id } = props; const threadInfo = useSelector(state => threadInfoSelector(state)[id]); const dispatch = useDispatch(); return ( ); }, ); function ColorEntity(props: { color: string }) { const colorStyle = { color: props.color }; return {props.color}; } const ConnectedRobotextMessage: React.ComponentType = React.memo( function ConnectedRobotextMessage(props) { const sidebarExistsOrCanBeCreated = useSidebarExistsOrCanBeCreated( props.threadInfo, props.item, ); return ( ); }, ); export default ConnectedRobotextMessage; diff --git a/web/chat/tooltip-utils.js b/web/chat/tooltip-utils.js index 60026aedc..2d4ab7f22 100644 --- a/web/chat/tooltip-utils.js +++ b/web/chat/tooltip-utils.js @@ -1,153 +1,177 @@ // @flow -import invariant from 'invariant'; import * as React from 'react'; -import { calculateMaxTextWidth } from '../utils/text-utils'; -import type { ItemAndContainerPositionInfo } from './position-types'; +import type { PositionInfo } from './position-types'; export const tooltipPositions = Object.freeze({ LEFT: 'left', RIGHT: 'right', - BOTTOM_LEFT: 'bottom-left', - BOTTOM_RIGHT: 'bottom-right', - TOP_LEFT: 'top-left', - TOP_RIGHT: 'top-right', + LEFT_BOTTOM: 'left-bottom', + RIGHT_BOTTOM: 'right-bottom', + LEFT_TOP: 'left-top', + RIGHT_TOP: 'right-top', + TOP: 'top', + BOTTOM: 'bottom', }); +type TooltipSize = { + +height: number, + +width: number, +}; + export type TooltipPositionStyle = { +xCoord: number, +yCoord: number, +verticalPosition: 'top' | 'bottom', +horizontalPosition: 'left' | 'right', +alignment: 'left' | 'center' | 'right', }; export type TooltipPosition = $Values; export type MessageTooltipAction = { +label: string, +onClick: (SyntheticEvent) => mixed, +actionButtonContent: React.Node, }; const sizeOfTooltipArrow = 10; // 7px arrow + 3px extra -const tooltipMenuItemHeight = 22; // 17px line-height + 5px padding bottom -const tooltipInnerTopPadding = 5; // 5px bottom is included in last item -const tooltipInnerPadding = 10; +const appTopBarHeight = 65; +// eslint-disable-next-line no-unused-vars const font = '14px -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", ' + '"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", ui-sans-serif'; type FindTooltipPositionArgs = { - +pointingToInfo: ItemAndContainerPositionInfo, - +tooltipTexts: $ReadOnlyArray, + +sourcePositionInfo: PositionInfo, + +tooltipSize: TooltipSize, +availablePositions: $ReadOnlyArray, - +layoutPosition: 'relative' | 'absolute', + +defaultPosition: TooltipPosition, + +preventDisplayingBelowSource?: boolean, }; + function findTooltipPosition({ - pointingToInfo, - tooltipTexts, + sourcePositionInfo, + tooltipSize, availablePositions, - layoutPosition, + defaultPosition, + preventDisplayingBelowSource, }: FindTooltipPositionArgs): TooltipPosition { - const { itemPosition: pointingTo, containerPosition } = pointingToInfo; + if (!window) { + return defaultPosition; + } + const appContainerPositionInfo: PositionInfo = { + height: window.innerHeight - appTopBarHeight, + width: window.innerWidth, + top: appTopBarHeight, + bottom: window.innerHeight, + left: 0, + right: window.innerWidth, + }; + + const pointingTo = sourcePositionInfo; const { - height: containerHeight, top: containerTop, - width: containerWidth, left: containerLeft, - } = containerPosition; - - const textWidth = calculateMaxTextWidth(tooltipTexts, font); - const width = textWidth + tooltipInnerPadding + sizeOfTooltipArrow; - const numberOfTooltipItems = tooltipTexts.length; - const tooltipHeight = - numberOfTooltipItems * tooltipMenuItemHeight + tooltipInnerTopPadding; - const heightWithArrow = tooltipHeight + sizeOfTooltipArrow; - - const absolutePositionedTooltip = layoutPosition === 'absolute'; - - let canBeDisplayedInLeftPosition, - canBeDisplayedInRightPosition, - canBeDisplayedInTopPosition, - canBeDisplayedInBottomPosition; - if (absolutePositionedTooltip) { - const pointingCenter = pointingTo.top + pointingTo.height / 2; - const currentTop = Math.max(pointingTo.top, 0); - const currentBottom = Math.min(pointingTo.bottom, containerHeight); - const currentPointing = Math.max( - Math.min(pointingCenter, containerHeight), - 0, - ); - const canBeDisplayedSideways = - currentPointing - tooltipHeight / 2 + containerTop >= 0 && - currentPointing + tooltipHeight / 2 + containerTop <= window.innerHeight; - - canBeDisplayedInLeftPosition = - pointingTo.left - width + containerLeft >= 0 && canBeDisplayedSideways; - canBeDisplayedInRightPosition = - pointingTo.right + width + containerLeft <= window.innerWidth && - canBeDisplayedSideways; - canBeDisplayedInTopPosition = - currentTop - heightWithArrow + containerTop >= 0; - canBeDisplayedInBottomPosition = - currentBottom + heightWithArrow + containerTop <= window.innerHeight; - } else { - const canBeDisplayedSideways = - pointingTo.top - (tooltipHeight - pointingTo.height) / 2 >= 0 && - pointingTo.bottom + (tooltipHeight - pointingTo.height) / 2 <= - containerHeight; - canBeDisplayedInLeftPosition = - pointingTo.left - width >= 0 && canBeDisplayedSideways; - canBeDisplayedInRightPosition = - pointingTo.right + width <= containerWidth && canBeDisplayedSideways; - canBeDisplayedInTopPosition = pointingTo.top - heightWithArrow >= 0; - canBeDisplayedInBottomPosition = - pointingTo.bottom + heightWithArrow <= containerHeight; - } + right: containerRight, + bottom: containerBottom, + } = appContainerPositionInfo; + + const tooltipWidth = tooltipSize.width; + const tooltipHeight = tooltipSize.height; + + const canBeDisplayedOnLeft = containerLeft + tooltipWidth <= pointingTo.left; + const canBeDisplayedOnRight = + tooltipWidth + pointingTo.right <= containerRight; + + const willCoverSidebarOnTopSideways = + preventDisplayingBelowSource && + pointingTo.top + tooltipHeight > pointingTo.bottom; + + const canBeDisplayedOnTopSideways = + pointingTo.top >= containerTop && + pointingTo.top + tooltipHeight <= containerBottom && + !willCoverSidebarOnTopSideways; + + const canBeDisplayedOnBottomSideways = + pointingTo.bottom <= containerBottom && + pointingTo.bottom - tooltipHeight >= containerTop; + + const verticalCenterOfPointingTo = pointingTo.top + pointingTo.height / 2; + const horizontalCenterOfPointingTo = pointingTo.left + pointingTo.width / 2; + + const willCoverSidebarInTheMiddleSideways = + preventDisplayingBelowSource && + verticalCenterOfPointingTo + tooltipHeight / 2 > pointingTo.bottom; + + const canBeDisplayedInTheMiddleSideways = + verticalCenterOfPointingTo - tooltipHeight / 2 >= containerTop && + verticalCenterOfPointingTo + tooltipHeight / 2 <= containerBottom && + !willCoverSidebarInTheMiddleSideways; + + const canBeDisplayedOnTop = + pointingTo.top - tooltipHeight >= containerTop && + horizontalCenterOfPointingTo - tooltipWidth / 2 >= containerLeft && + horizontalCenterOfPointingTo + tooltipWidth / 2 <= containerRight; + + const canBeDisplayedOnBottom = + pointingTo.bottom + tooltipHeight <= containerBottom && + horizontalCenterOfPointingTo - tooltipWidth / 2 >= containerLeft && + horizontalCenterOfPointingTo + tooltipWidth / 2 <= containerRight && + !preventDisplayingBelowSource; for (const tooltipPosition of availablePositions) { - invariant( - numberOfTooltipItems === 1 || - (tooltipPosition !== tooltipPositions.RIGHT && - tooltipPosition !== tooltipPositions.LEFT), - `${tooltipPosition} position can be used only for single element tooltip`, - ); if ( tooltipPosition === tooltipPositions.RIGHT && - canBeDisplayedInRightPosition + canBeDisplayedOnRight && + canBeDisplayedInTheMiddleSideways ) { return tooltipPosition; } else if ( - tooltipPosition === tooltipPositions.BOTTOM_RIGHT && - canBeDisplayedInBottomPosition + tooltipPosition === tooltipPositions.RIGHT_BOTTOM && + canBeDisplayedOnRight && + canBeDisplayedOnBottomSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.LEFT && - canBeDisplayedInLeftPosition + canBeDisplayedOnLeft && + canBeDisplayedInTheMiddleSideways + ) { + return tooltipPosition; + } else if ( + tooltipPosition === tooltipPositions.LEFT_BOTTOM && + canBeDisplayedOnLeft && + canBeDisplayedOnBottomSideways + ) { + return tooltipPosition; + } else if ( + tooltipPosition === tooltipPositions.LEFT_TOP && + canBeDisplayedOnLeft && + canBeDisplayedOnTopSideways ) { return tooltipPosition; } else if ( - tooltipPosition === tooltipPositions.BOTTOM_LEFT && - canBeDisplayedInBottomPosition + tooltipPosition === tooltipPositions.RIGHT_TOP && + canBeDisplayedOnRight && + canBeDisplayedOnTopSideways ) { return tooltipPosition; } else if ( - tooltipPosition === tooltipPositions.TOP_LEFT && - canBeDisplayedInTopPosition + tooltipPosition === tooltipPositions.TOP && + canBeDisplayedOnTop ) { return tooltipPosition; } else if ( - tooltipPosition === tooltipPositions.TOP_RIGHT && - canBeDisplayedInTopPosition + tooltipPosition === tooltipPositions.BOTTOM && + canBeDisplayedOnBottom ) { return tooltipPosition; } } - return availablePositions[availablePositions.length - 1]; + return defaultPosition; } export { findTooltipPosition, sizeOfTooltipArrow }; diff --git a/web/chat/tooltip.react.js b/web/chat/tooltip.react.js index 23922585c..46c346ad4 100644 --- a/web/chat/tooltip.react.js +++ b/web/chat/tooltip.react.js @@ -1,105 +1,86 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import type { ItemAndContainerPositionInfo } from './position-types'; -import { findTooltipPosition, type TooltipPosition } from './tooltip-utils'; +import { type TooltipPosition, tooltipPositions } from './tooltip-utils'; import css from './tooltip.css'; type Style = { +left?: number, +right?: number, +top?: number, +bottom?: number, }; export type TooltipStyle = { +className: string, +style?: Style }; type TooltipMenuProps = { +availableTooltipPositions: $ReadOnlyArray, +targetPositionInfo: ItemAndContainerPositionInfo, +layoutPosition: 'relative' | 'absolute', +getStyle: (tooltipPosition: TooltipPosition) => TooltipStyle, +children: React.ChildrenArray< React.Element, >, }; function TooltipMenu(props: TooltipMenuProps): React.Node { - const { - availableTooltipPositions, - targetPositionInfo, - layoutPosition, - getStyle, - children, - } = props; + const { getStyle, children } = props; + // eslint-disable-next-line no-unused-vars const tooltipTexts = React.useMemo( () => React.Children.map(children, item => item.props.text), [children], ); - const tooltipPosition = React.useMemo( - () => - findTooltipPosition({ - pointingToInfo: targetPositionInfo, - tooltipTexts, - availablePositions: availableTooltipPositions, - layoutPosition, - }), - [ - tooltipTexts, - targetPositionInfo, - availableTooltipPositions, - layoutPosition, - ], - ); + const tooltipPosition = React.useMemo(() => tooltipPositions.LEFT, []); const tooltipStyle = React.useMemo(() => getStyle(tooltipPosition), [ getStyle, tooltipPosition, ]); const className = React.useMemo( () => classNames( css.messageTooltipMenu, tooltipStyle.className, css.messageTimestampTooltip, ), [tooltipStyle], ); const style = tooltipStyle.style ? tooltipStyle.style : null; return (
    {children}
); } type TooltipButtonProps = { +onClick: (event: SyntheticEvent) => void, +text: string, }; function TooltipButton(props: TooltipButtonProps): React.Node { const { onClick, text } = props; return (
  • ); } type TooltipTextItemProps = { +text: string, }; function TooltipTextItem(props: TooltipTextItemProps): React.Node { return (
  • {props.text}
  • ); } export { TooltipMenu, TooltipButton, TooltipTextItem }; diff --git a/web/utils/tooltip-utils.test.js b/web/utils/tooltip-utils.test.js new file mode 100644 index 000000000..153b14ae8 --- /dev/null +++ b/web/utils/tooltip-utils.test.js @@ -0,0 +1,204 @@ +// @flow + +import type { PositionInfo } from '../chat/position-types'; +import { findTooltipPosition, tooltipPositions } from '../chat/tooltip-utils'; + +const QHDWindow = { + width: 2560, + height: 1440, +}; + +const tooltipSourcePositionCenter: PositionInfo = { + width: 200, + height: 300, + left: QHDWindow.width / 2 - 100, + top: QHDWindow.height / 2 - 150, + right: QHDWindow.width / 2 + 100, + bottom: QHDWindow.height / 2 + 150, +}; + +const tooltipSourcePositionTopRight: PositionInfo = { + width: 200, + height: 300, + left: QHDWindow.width - 200, + top: 65, // app top bar height + right: QHDWindow.width, + bottom: 300 + 65, // tooltip height + app top bar height +}; + +const tooltipSourcePositionBottomLeft: PositionInfo = { + width: 200, + height: 300, + left: 0, + top: QHDWindow.height - 300, + right: 200, + bottom: QHDWindow.height, +}; + +const tooltipSizeSmall = { + width: 100, + height: 200, +}; + +const tooltipSizeBig = { + width: 300, + height: 500, +}; + +const allTooltipPositions = [ + tooltipPositions.LEFT, + tooltipPositions.LEFT_TOP, + tooltipPositions.LEFT_BOTTOM, + tooltipPositions.RIGHT, + tooltipPositions.RIGHT_TOP, + tooltipPositions.RIGHT_BOTTOM, + tooltipPositions.TOP, + tooltipPositions.BOTTOM, +]; + +const sidewaysTooltipPositions = [ + tooltipPositions.LEFT, + tooltipPositions.LEFT_TOP, + tooltipPositions.LEFT_BOTTOM, + tooltipPositions.RIGHT, + tooltipPositions.RIGHT_TOP, + tooltipPositions.RIGHT_BOTTOM, +]; + +const topAndBottomTooltipPositions = [ + tooltipPositions.TOP, + tooltipPositions.BOTTOM, +]; + +const onlyLeftTooltipPositions = [ + tooltipPositions.LEFT, + tooltipPositions.LEFT_BOTTOM, + tooltipPositions.LEFT_TOP, +]; + +beforeAll(() => { + window.innerWidth = QHDWindow.width; + window.innerHeight = QHDWindow.height; +}); + +afterAll(() => { + window.innerWidth = 1024; + window.innerHeight = 768; +}); + +describe('findTooltipPosition', () => { + it( + 'should return first position if there is enough space ' + + 'in every direction', + () => + expect( + findTooltipPosition({ + sourcePositionInfo: tooltipSourcePositionCenter, + tooltipSize: tooltipSizeSmall, + availablePositions: allTooltipPositions, + defaultPosition: allTooltipPositions[0], + }), + ).toMatch(allTooltipPositions[0]), + ); + + it( + 'should return first non-left position ' + + 'if there is no space on the left', + () => + expect( + findTooltipPosition({ + sourcePositionInfo: tooltipSourcePositionBottomLeft, + tooltipSize: tooltipSizeSmall, + availablePositions: sidewaysTooltipPositions, + defaultPosition: sidewaysTooltipPositions[0], + }), + ).toMatch(tooltipPositions.RIGHT), + ); + + it('should return bottom position if there is no space on the top ', () => + expect( + findTooltipPosition({ + sourcePositionInfo: tooltipSourcePositionTopRight, + tooltipSize: tooltipSizeSmall, + availablePositions: topAndBottomTooltipPositions, + defaultPosition: topAndBottomTooltipPositions[0], + }), + ).toMatch(tooltipPositions.BOTTOM)); + + it( + 'should return top left position if the tooltip is higher than the ' + + 'source object and there is no enough space on the top', + () => + expect( + findTooltipPosition({ + sourcePositionInfo: tooltipSourcePositionTopRight, + tooltipSize: tooltipSizeBig, + availablePositions: onlyLeftTooltipPositions, + defaultPosition: onlyLeftTooltipPositions[0], + }), + ).toMatch(tooltipPositions.LEFT_TOP), + ); + + it( + 'should return bottom position on left ' + + 'to prevent covering element below source', + () => + expect( + findTooltipPosition({ + sourcePositionInfo: tooltipSourcePositionCenter, + tooltipSize: tooltipSizeBig, + availablePositions: onlyLeftTooltipPositions, + defaultPosition: onlyLeftTooltipPositions[0], + preventDisplayingBelowSource: true, + }), + ).toMatch(tooltipPositions.LEFT_BOTTOM), + ); + + it( + 'should return first position ' + + 'that does not cover element below source ', + () => + expect( + findTooltipPosition({ + sourcePositionInfo: tooltipSourcePositionCenter, + tooltipSize: tooltipSizeBig, + availablePositions: [ + tooltipPositions.BOTTOM, + tooltipPositions.RIGHT, + tooltipPositions.RIGHT_TOP, + tooltipPositions.LEFT, + tooltipPositions.LEFT_TOP, + tooltipPositions.TOP, + tooltipPositions.LEFT_BOTTOM, + ], + defaultPosition: tooltipPositions.BOTTOM, + preventDisplayingBelowSource: true, + }), + ).toMatch(tooltipPositions.TOP), + ); + + it( + 'should return default position ' + + 'if an empty array of available is provided', + () => + expect( + findTooltipPosition({ + sourcePositionInfo: tooltipSourcePositionCenter, + tooltipSize: tooltipSizeSmall, + availablePositions: [], + defaultPosition: tooltipPositions.LEFT_BOTTOM, + }), + ).toMatch(tooltipPositions.LEFT_BOTTOM), + ); + + it('should return default position if an no position is available', () => + expect( + findTooltipPosition({ + sourcePositionInfo: tooltipSourcePositionTopRight, + tooltipSize: tooltipSizeBig, + availablePositions: allTooltipPositions, + defaultPosition: tooltipPositions.BOTTOM, + preventDisplayingBelowSource: true, + }), + ).toMatch(tooltipPositions.BOTTOM)); +});