diff --git a/web/chat/message-timestamp-tooltip.react.js b/web/chat/message-timestamp-tooltip.react.js index 4bdbfddf1..815c6f5e2 100644 --- a/web/chat/message-timestamp-tooltip.react.js +++ b/web/chat/message-timestamp-tooltip.react.js @@ -1,146 +1,146 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { isComposableMessageType } from 'lib/types/message-types'; import { longAbsoluteDate } from 'lib/utils/date-utils'; import css from './chat-message-list.css'; import type { OnMessagePositionWithContainerInfo } from './position-types'; import { type TooltipPosition, tooltipPositions, findTooltipPosition, sizeOfTooltipArrow, } from './tooltip-utils'; const availablePositionsForComposedViewerMessage = [ tooltipPositions.BOTTOM_RIGHT, ]; const availablePositionsForNonComposedOrNonViewerMessage = [ tooltipPositions.LEFT, ]; type Props = {| +messagePositionInfo: OnMessagePositionWithContainerInfo, +timeZone: ?string, |}; function MessageTimestampTooltip(props: Props) { 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 tooltipPosition = React.useMemo( () => findTooltipPosition({ pointingToInfo, - text, + tooltipTexts: [text], availablePositions: availableTooltipPositions, layoutPosition: 'absolute', }), [availableTooltipPositions, pointingToInfo, text], ); const { style, className } = React.useMemo( () => getTimestampTooltipStyle(messagePositionInfo, tooltipPosition), [messagePositionInfo, tooltipPosition], ); return (
{text}
); } function getTimestampTooltipStyle( messagePositionInfo: OnMessagePositionWithContainerInfo, tooltipPosition: TooltipPosition, ) { 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, top: tooltipPointing, }; 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) { const tooltipPointing = Math.min( containerHeight - messagePosition.top, containerHeight, ); style = { left: messagePosition.left, bottom: tooltipPointing + sizeOfTooltipArrow, }; className = css.messageTopLeftTooltip; } else if (tooltipPosition === tooltipPositions.TOP_RIGHT) { 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) { const tooltipPointing = Math.min(messagePosition.bottom, containerHeight); style = { left: messagePosition.left, top: tooltipPointing + sizeOfTooltipArrow, }; className = css.messageBottomLeftTooltip; } else if (tooltipPosition === tooltipPositions.BOTTOM_RIGHT) { const tooltipPointing = Math.min(messagePosition.bottom, containerHeight); style = { right: containerWidth - messagePosition.right, top: tooltipPointing + sizeOfTooltipArrow, }; className = css.messageBottomRightTooltip; } return { style, className }; } export default MessageTimestampTooltip; diff --git a/web/chat/sidebar-tooltip.react.js b/web/chat/sidebar-tooltip.react.js index a3af480b1..d83abf310 100644 --- a/web/chat/sidebar-tooltip.react.js +++ b/web/chat/sidebar-tooltip.react.js @@ -1,287 +1,287 @@ // @flow import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; import invariant from 'invariant'; import * as React from 'react'; import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors'; import { type ComposableMessageInfo, type RobotextMessageInfo, } from 'lib/types/message-types'; import type { ThreadInfo } from 'lib/types/thread-types'; import { useOnClickThread, useOnClickPendingSidebar, } from '../selectors/nav-selectors'; import css from './chat-message-list.css'; import type { ItemAndContainerPositionInfo, PositionInfo, } from './position-types'; import { findTooltipPosition, tooltipPositions, type TooltipPosition, } from './tooltip-utils'; const ellipsisIconExcessVerticalWhitespace = 10; type Props = {| +onLeave: () => void, +onButtonClick: (event: SyntheticEvent) => void, +buttonText: string, +containerPosition: PositionInfo, +availableTooltipPositions: $ReadOnlyArray, |}; function SidebarTooltipButton(props: Props) { const { onLeave, onButtonClick, buttonText, containerPosition, availableTooltipPositions, } = props; const [tooltipVisible, setTooltipVisible] = React.useState(false); const [pointingTo, setPointingTo] = React.useState(); const toggleMenu = React.useCallback( (event: SyntheticEvent) => { setTooltipVisible(!tooltipVisible); if (tooltipVisible) { return; } const rect = event.currentTarget.getBoundingClientRect(); const { top, bottom, left, right, width, height } = rect; const dotsPosition: ItemAndContainerPositionInfo = { containerPosition, itemPosition: { top: top - containerPosition.top + ellipsisIconExcessVerticalWhitespace, bottom: bottom - containerPosition.top - ellipsisIconExcessVerticalWhitespace, left: left - containerPosition.left, right: right - containerPosition.left, width, height: height - ellipsisIconExcessVerticalWhitespace * 2, }, }; setPointingTo(dotsPosition); }, [containerPosition, tooltipVisible], ); const toggleSidebar = React.useCallback( (event: SyntheticEvent) => { onButtonClick(event); onLeave(); }, [onLeave, onButtonClick], ); const hideMenu = React.useCallback(() => { setTooltipVisible(false); }, []); const tooltipPosition = React.useMemo(() => { if (!pointingTo) { return null; } return findTooltipPosition({ pointingToInfo: pointingTo, - text: buttonText, + tooltipTexts: [buttonText], availablePositions: availableTooltipPositions, layoutPosition: 'relative', }); }, [availableTooltipPositions, pointingTo, buttonText]); const sidebarTooltip = React.useMemo(() => { if (!tooltipVisible || !tooltipPosition) { return null; } return ( ); }, [buttonText, toggleSidebar, tooltipPosition, tooltipVisible]); return (
{sidebarTooltip}
); } type TooltipButtonProps = {| +onButtonClick: (event: SyntheticEvent) => void, +buttonText: string, +tooltipPosition: TooltipPosition, |}; function TooltipButton(props: TooltipButtonProps) { const { onButtonClick, buttonText, tooltipPosition } = props; const sidebarStyle = React.useMemo( () => getSidebarTooltipStyle(tooltipPosition), [tooltipPosition], ); const sidebarMenuClassName = React.useMemo( () => classNames(css.menuSidebarContent, sidebarStyle), [sidebarStyle], ); return (
); } const openSidebarText = 'Go to sidebar'; type OpenSidebarProps = {| +threadCreatedFromMessage: ThreadInfo, +onLeave: () => void, +containerPosition: PositionInfo, +availableTooltipPositions: $ReadOnlyArray, |}; function OpenSidebar(props: OpenSidebarProps) { const { threadCreatedFromMessage, onLeave, containerPosition, availableTooltipPositions, } = props; const onButtonClick = useOnClickThread(threadCreatedFromMessage.id); return ( ); } const createSidebarText = 'Create sidebar'; type CreateSidebarProps = {| +threadInfo: ThreadInfo, +messageInfo: ComposableMessageInfo | RobotextMessageInfo, +onLeave: () => void, +containerPosition: PositionInfo, +availableTooltipPositions: $ReadOnlyArray, |}; function CreateSidebar(props: CreateSidebarProps) { const { threadInfo, messageInfo, containerPosition, availableTooltipPositions, } = props; const onButtonClick = useOnClickPendingSidebar(messageInfo, threadInfo); return ( ); } type MessageActionTooltipProps = {| +threadInfo: ThreadInfo, +item: ChatMessageInfoItem, +onLeave: () => void, +containerPosition: PositionInfo, +availableTooltipPositions: $ReadOnlyArray, |}; function MessageActionTooltip(props: MessageActionTooltipProps) { const { threadInfo, item, onLeave, containerPosition, availableTooltipPositions, } = props; if (item.threadCreatedFromMessage) { return ( ); } else { return ( ); } } function getSidebarTooltipStyle(tooltipPosition: TooltipPosition): string { let className; if (tooltipPosition === tooltipPositions.TOP_RIGHT) { className = classNames( css.menuSidebarTopRightTooltip, css.messageTopRightTooltip, css.menuSidebarExtraAreaTop, css.menuSidebarExtraAreaTopRight, ); } else if (tooltipPosition === tooltipPositions.TOP_LEFT) { className = classNames( css.menuSidebarTopLeftTooltip, css.messageTopLeftTooltip, css.menuSidebarExtraAreaTop, css.menuSidebarExtraAreaTopLeft, ); } else if (tooltipPosition === tooltipPositions.RIGHT) { className = classNames( css.menuSidebarRightTooltip, css.messageRightTooltip, css.menuSidebarExtraArea, css.menuSidebarExtraAreaRight, ); } else if (tooltipPosition === tooltipPositions.LEFT) { className = classNames( css.menuSidebarLeftTooltip, css.messageLeftTooltip, css.menuSidebarExtraArea, css.menuSidebarExtraAreaLeft, ); } invariant(className, `${tooltipPosition} is not valid for sidebar tooltip`); return className; } export default MessageActionTooltip; diff --git a/web/chat/tooltip-utils.js b/web/chat/tooltip-utils.js index f1e2816e1..7dfcebd5b 100644 --- a/web/chat/tooltip-utils.js +++ b/web/chat/tooltip-utils.js @@ -1,132 +1,136 @@ // @flow -import { calculateTextWidth } from '../utils/text-utils'; +import invariant from 'invariant'; + +import { calculateMaxTextWidth } from '../utils/text-utils'; import type { ItemAndContainerPositionInfo } 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', }); export type TooltipPosition = $Values; const sizeOfTooltipArrow = 10; // 7px arrow + 3px extra -const tooltipHeight = 27; // 17px line-height + 10px padding -const heightWithArrow = tooltipHeight + sizeOfTooltipArrow; +const tooltipMenuItemHeight = 27; // 17px line-height + 10px padding const tooltipInnerPadding = 10; const font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", ' + '"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", ' + '"Helvetica Neue", ui-sans-serif'; type FindTooltipPositionArgs = {| +pointingToInfo: ItemAndContainerPositionInfo, - +text: string, + +tooltipTexts: $ReadOnlyArray, +availablePositions: $ReadOnlyArray, +layoutPosition: 'relative' | 'absolute', |}; function findTooltipPosition({ pointingToInfo, - text, + tooltipTexts, availablePositions, layoutPosition, }: FindTooltipPositionArgs) { const { itemPosition: pointingTo, containerPosition } = pointingToInfo; const { height: containerHeight, top: containerTop, width: containerWidth, left: containerLeft, } = containerPosition; - const textWidth = calculateTextWidth(text, font); + const textWidth = calculateMaxTextWidth(tooltipTexts, font); const width = textWidth + tooltipInnerPadding + sizeOfTooltipArrow; + const numberOfTooltipItems = tooltipTexts.length; + const tooltipHeight = numberOfTooltipItems * tooltipMenuItemHeight; + 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; } 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 ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.BOTTOM_RIGHT && canBeDisplayedInBottomPosition ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.LEFT && canBeDisplayedInLeftPosition ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.BOTTOM_LEFT && canBeDisplayedInBottomPosition ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.TOP_LEFT && canBeDisplayedInTopPosition ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.TOP_RIGHT && canBeDisplayedInTopPosition ) { return tooltipPosition; } } return availablePositions[availablePositions.length - 1]; } -export { - findTooltipPosition, - sizeOfTooltipArrow, - tooltipHeight, - heightWithArrow, - tooltipInnerPadding, -}; +export { findTooltipPosition, sizeOfTooltipArrow, tooltipInnerPadding }; diff --git a/web/utils/text-utils.js b/web/utils/text-utils.js index 3ec872f5f..e344db152 100644 --- a/web/utils/text-utils.js +++ b/web/utils/text-utils.js @@ -1,15 +1,19 @@ // @flow let canvas; -function calculateTextWidth(text: string, font: string): number { +function calculateMaxTextWidth( + texts: $ReadOnlyArray, + font: string, +): number { if (!canvas) { canvas = document.createElement('canvas'); } const context = canvas.getContext('2d'); context.font = font; - const { width } = context.measureText(text); - return width; + + const widths = texts.map((text) => context.measureText(text).width); + return Math.max(...widths); } -export { calculateTextWidth }; +export { calculateMaxTextWidth };