diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js --- a/web/chat/composed-message.react.js +++ b/web/chat/composed-message.react.js @@ -26,11 +26,11 @@ 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, ]; diff --git a/web/chat/message-timestamp-tooltip.react.js b/web/chat/message-timestamp-tooltip.react.js --- a/web/chat/message-timestamp-tooltip.react.js +++ b/web/chat/message-timestamp-tooltip.react.js @@ -21,7 +21,7 @@ const availablePositionsForComposedViewerMessage = [tooltipPositions.LEFT]; const availablePositionsForNonComposedOrNonViewerMessage = [ - tooltipPositions.BOTTOM_RIGHT, + tooltipPositions.RIGHT_BOTTOM, ]; type Props = { @@ -99,7 +99,7 @@ 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, @@ -109,7 +109,7 @@ 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, @@ -119,14 +119,14 @@ 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), diff --git a/web/chat/robotext-message.react.js b/web/chat/robotext-message.react.js --- a/web/chat/robotext-message.react.js +++ b/web/chat/robotext-message.react.js @@ -26,7 +26,7 @@ // eslint-disable-next-line no-unused-vars const availableTooltipPositionsForRobotext = [ - tooltipPositions.TOP_RIGHT, + tooltipPositions.RIGHT_TOP, tooltipPositions.RIGHT, tooltipPositions.LEFT, ]; diff --git a/web/chat/tooltip-utils.js b/web/chat/tooltip-utils.js --- a/web/chat/tooltip-utils.js +++ b/web/chat/tooltip-utils.js @@ -1,20 +1,25 @@ // @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, @@ -32,122 +37,141 @@ }; 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 --- a/web/chat/tooltip.react.js +++ b/web/chat/tooltip.react.js @@ -4,7 +4,7 @@ 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 = { @@ -25,34 +25,15 @@ >, }; 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, diff --git a/web/utils/tooltip-utils.test.js b/web/utils/tooltip-utils.test.js new file mode 100644 --- /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)); +});