diff --git a/web/chat/chat-constants.js b/web/chat/chat-constants.js new file mode 100644 index 000000000..9b3fdc9a1 --- /dev/null +++ b/web/chat/chat-constants.js @@ -0,0 +1,18 @@ +// @flow + +export const tooltipStyle = { + paddingLeft: 5, + paddingRight: 5, + rowGap: 3, +}; + +export const tooltipLabelStyle = { + padding: 6, + height: 20, +}; +export const tooltipButtonStyle = { + paddingLeft: 6, + paddingRight: 6, + width: 30, + height: 38, +}; diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js index 293183a1b..7a83be449 100644 --- a/web/chat/composed-message.react.js +++ b/web/chat/composed-message.react.js @@ -1,245 +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.LEFT, ]; const availableTooltipPositionsForNonViewerMessage = [ tooltipPositions.TOP_LEFT, 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; - const messageTooltipProps = { - threadInfo, - item, - availableTooltipPositions, - mouseOverMessagePosition: this.props.mouseOverMessagePosition, - }; - - if (this.props.canReply) { - messageTooltip = ( - - ); - } else { - messageTooltip = ( - - ); - } + 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-tooltip.css b/web/chat/message-tooltip.css index b451ef762..dbee9f7b3 100644 --- a/web/chat/message-tooltip.css +++ b/web/chat/message-tooltip.css @@ -1,82 +1,60 @@ +div.messageTooltipContainer { + display: flex; + flex-direction: column; + align-items: center; + font-size: var(--s-font-14); +} + div.messageActionContainer { display: flex; flex-direction: row; align-items: center; justify-content: center; - padding: 0 6px; background-color: var(--message-action-tooltip-bg); border-radius: 8px; width: fit-content; } div.messageActionButtons { display: flex; font-size: 16px; } div.messageActionButtons svg { padding: 10px 6px 6px; color: var(--color-disabled); } + div.messageActionButtons svg:hover { cursor: pointer; color: var(--fg); } -div.messageActionButtonsViewer { - flex-direction: row; - margin-left: auto; - margin-right: 0; -} -div.messageActionButtonsNonViewer { - flex-direction: row-reverse; - margin-left: 0; - margin-right: auto; -} -div.messageActionLinkIcon { - margin: 0 3px; - position: relative; -} -div.messageActionExtraAreaTop:before { - height: 15px; - width: 55px; - content: ''; - position: absolute; - bottom: -15px; -} -div.messageActionExtraAreaTopRight:before { - right: 0; -} -div.messageActionExtraAreaTopLeft:before { - left: 0; -} -div.messageActionExtraArea:before { - height: 30px; - width: 20px; - content: ''; - position: absolute; -} -div.messageActionExtraAreaRight:before { - left: -20px; -} -div.messageActionExtraAreaLeft:before { - right: -20px; + +div.messageTooltipButton { + display: flex; + align-items: center; + justify-content: center; } -div.messageActionTopRightTooltip { - bottom: 100%; - margin-bottom: 1px; - right: 0; + +div.messageTooltipLabel { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + background-color: var(--message-action-tooltip-bg); + color: var(--tool-tip-color); + border-radius: 8px; + overflow: auto; + white-space: nowrap; } -div.messageActionTopLeftTooltip { - bottom: 100%; - margin-bottom: 1px; - left: 0; + +div.leftTooltipAlign { + align-items: flex-start; } -div.messageActionLeftTooltip { - top: 50%; - right: 100%; - margin-right: 7px; + +div.centerTooltipAlign { + align-items: center; } -div.messageActionRightTooltip { - top: 50%; - left: 100%; - margin-left: 7px; + +div.rightTooltipAlign { + align-items: flex-end; } diff --git a/web/chat/message-tooltip.react.js b/web/chat/message-tooltip.react.js index 1fe2db475..42ab6cc5c 100644 --- a/web/chat/message-tooltip.react.js +++ b/web/chat/message-tooltip.react.js @@ -1,269 +1,115 @@ // @flow import classNames from 'classnames'; -import invariant from 'invariant'; import * as React from 'react'; -import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors'; -import { useSidebarExistsOrCanBeCreated } from 'lib/shared/thread-utils'; -import type { ThreadInfo } from 'lib/types/thread-types'; - -import CommIcon from '../CommIcon.react.js'; -import type { InputState } from '../input/input-state'; import { - useOnClickThread, - useOnClickPendingSidebar, -} from '../selectors/nav-selectors'; -import MessageReplyButton from './message-reply-button.react'; + tooltipButtonStyle, + tooltipLabelStyle, + tooltipStyle, +} from './chat-constants'; import css from './message-tooltip.css'; -import type { - ItemAndContainerPositionInfo, - MessagePositionInfo, - OnMessagePositionWithContainerInfo, - PositionInfo, -} from './position-types'; -import { tooltipPositions, type TooltipPosition } from './tooltip-utils'; -import { - TooltipMenu, - type TooltipStyle, - TooltipTextItem, -} from './tooltip.react'; - -const messageActionIconExcessVerticalWhitespace = 10; - -const openSidebarText = 'Go to thread'; -const createSidebarText = 'Create thread'; - -type TooltipType = 'sidebar' | 'reply'; +import { type MessageTooltipAction } from './tooltip-utils'; -type BaseMessageTooltipProps = { - +threadInfo: ThreadInfo, - +item: ChatMessageInfoItem, - +availableTooltipPositions: $ReadOnlyArray, - +mouseOverMessagePosition: OnMessagePositionWithContainerInfo, +type MessageTooltipProps = { + +actions: $ReadOnlyArray, + +messageTimestamp: string, + +alignment?: 'left' | 'center' | 'right', }; -type MessageTooltipProps = - | { - ...BaseMessageTooltipProps, - +canReply: false, - } - | { - ...BaseMessageTooltipProps, - +canReply: true, - +inputState: ?InputState, - +setMouseOverMessagePosition: ( - messagePositionInfo: MessagePositionInfo, - ) => void, - }; function MessageTooltip(props: MessageTooltipProps): React.Node { - const { - threadInfo, - item, - availableTooltipPositions, - mouseOverMessagePosition, - canReply, - } = props; - - const { containerPosition } = mouseOverMessagePosition; - - const [activeTooltip, setActiveTooltip] = React.useState(); - const [pointingTo, setPointingTo] = React.useState(); - - const showTooltip = React.useCallback( - (tooltipType: TooltipType, iconPosition: ItemAndContainerPositionInfo) => { - if (activeTooltip) { - return; - } - setActiveTooltip(tooltipType); - setPointingTo(iconPosition); - }, - [activeTooltip], + const { actions, messageTimestamp, alignment = 'left' } = props; + const [activeTooltipLabel, setActiveTooltipLabel] = React.useState(); + const messageActionButtonsContainerClassName = classNames( + css.messageActionContainer, + css.messageActionButtons, ); - const hideTooltip = React.useCallback(() => { - setActiveTooltip(null); - }, []); + const messageTooltipButtonStyle = React.useMemo(() => tooltipButtonStyle, []); - const showSidebarTooltip = React.useCallback( - (event: SyntheticEvent) => { - const rect = event.currentTarget.getBoundingClientRect(); - const iconPosition = getIconPosition(rect, containerPosition); - showTooltip('sidebar', iconPosition); - }, - [containerPosition, showTooltip], - ); - - const showReplyTooltip = React.useCallback( - (event: SyntheticEvent) => { - const rect = event.currentTarget.getBoundingClientRect(); - const iconPosition = getIconPosition(rect, containerPosition); - showTooltip('reply', iconPosition); - }, - [containerPosition, showTooltip], - ); - - const { threadCreatedFromMessage, messageInfo } = item; - - const onThreadOpen = useOnClickThread(threadCreatedFromMessage); - const onPendingSidebarOpen = useOnClickPendingSidebar( - messageInfo, - threadInfo, - ); - const onSidebarButtonClick = React.useCallback( - (event: SyntheticEvent) => { - if (threadCreatedFromMessage) { - onThreadOpen(event); - } else { - onPendingSidebarOpen(event); - } - }, - [onPendingSidebarOpen, onThreadOpen, threadCreatedFromMessage], - ); - - const setMouseOverMessagePosition = props.canReply - ? props.setMouseOverMessagePosition - : null; - - const onReplyButtonClick = React.useCallback(() => { - setMouseOverMessagePosition?.({ - type: 'off', - item: item, + const tooltipButtons = React.useMemo(() => { + if (!actions || actions.length === 0) { + return null; + } + const buttons = actions.map(({ label, onClick, actionButtonContent }) => { + const onMouseEnter = () => { + setActiveTooltipLabel(label); + }; + const onMouseLeave = () => + setActiveTooltipLabel(oldLabel => + label === oldLabel ? null : oldLabel, + ); + + return ( +
+ {actionButtonContent} +
+ ); }); - }, [item, setMouseOverMessagePosition]); - - let tooltipText = ''; - if (activeTooltip === 'reply') { - tooltipText = 'Reply'; - } else if (activeTooltip === 'sidebar') { - tooltipText = threadCreatedFromMessage - ? openSidebarText - : createSidebarText; - } - - let tooltipMenu = null; - if (pointingTo && activeTooltip) { - tooltipMenu = ( - - - + return ( +
{buttons}
); - } + }, [ + actions, + messageActionButtonsContainerClassName, + messageTooltipButtonStyle, + ]); + + const messageTooltipLabelStyle = React.useMemo(() => tooltipLabelStyle, []); + const messageTooltipTopLabelStyle = React.useMemo( + () => ({ + height: `${tooltipLabelStyle.height + 2 * tooltipLabelStyle.padding}px`, + }), + [], + ); - let replyButton; - if (canReply) { - invariant(props.inputState, 'inputState must be set if replyButton exists'); - replyButton = ( -
- - {activeTooltip === 'reply' ? tooltipMenu : null} + const tooltipLabel = React.useMemo(() => { + if (!activeTooltipLabel) { + return null; + } + return ( +
+ {activeTooltipLabel}
); - } + }, [activeTooltipLabel, messageTooltipLabelStyle]); - const sidebarExistsOrCanBeCreated = useSidebarExistsOrCanBeCreated( - threadInfo, - item, - ); - - let sidebarButton; - if (sidebarExistsOrCanBeCreated) { - sidebarButton = ( -
- - {activeTooltip === 'sidebar' ? tooltipMenu : null} + const tooltipTimestamp = React.useMemo(() => { + if (!messageTimestamp) { + return null; + } + return ( +
+ {messageTimestamp}
); - } + }, [messageTimestamp, messageTooltipLabelStyle]); + + const messageTooltipContainerStyle = React.useMemo(() => tooltipStyle, []); + + const containerClassNames = React.useMemo( + () => + classNames(css.messageTooltipContainer, { + [css.leftTooltipAlign]: alignment === 'left', + [css.centerTooltipAlign]: alignment === 'center', + [css.rightTooltipAlign]: alignment === 'right', + }), + [alignment], + ); - const { isViewer } = messageInfo.creator; - const messageActionButtonsContainerClassName = classNames({ - [css.messageActionContainer]: true, - [css.messageActionButtons]: true, - [css.messageActionButtonsViewer]: isViewer, - [css.messageActionButtonsNonViewer]: !isViewer, - }); return ( -
-
- {sidebarButton} - {replyButton} -
+
+
{tooltipLabel}
+ {tooltipButtons} + {tooltipTimestamp}
); } -function getIconPosition( - rect: ClientRect, - containerPosition: PositionInfo, -): ItemAndContainerPositionInfo { - const { top, bottom, left, right, width, height } = rect; - return { - containerPosition, - itemPosition: { - top: - top - containerPosition.top + messageActionIconExcessVerticalWhitespace, - bottom: - bottom - - containerPosition.top - - messageActionIconExcessVerticalWhitespace, - left: left - containerPosition.left, - right: right - containerPosition.left, - width, - height: height - messageActionIconExcessVerticalWhitespace * 2, - }, - }; -} - -function getMessageActionTooltipStyle( - tooltipPosition: TooltipPosition, -): TooltipStyle { - let className; - if (tooltipPosition === tooltipPositions.TOP_RIGHT) { - className = classNames( - css.messageActionTopRightTooltip, - css.messageActionExtraAreaTop, - css.messageActionExtraAreaTopRight, - ); - } else if (tooltipPosition === tooltipPositions.TOP_LEFT) { - className = classNames( - css.messageActionTopLeftTooltip, - css.messageActionExtraAreaTop, - css.messageActionExtraAreaTopLeft, - ); - } else if (tooltipPosition === tooltipPositions.RIGHT) { - className = classNames( - css.messageActionRightTooltip, - css.messageActionExtraArea, - css.messageActionExtraAreaRight, - ); - } else if (tooltipPosition === tooltipPositions.LEFT) { - className = classNames( - css.messageActionLeftTooltip, - css.messageActionExtraArea, - css.messageActionExtraAreaLeft, - ); - } - - invariant(className, `${tooltipPosition} is not valid for message tooltip`); - return { className }; -} - export default MessageTooltip; diff --git a/web/chat/robotext-message.react.js b/web/chat/robotext-message.react.js index cae59f832..fdbc0894b 100644 --- a/web/chat/robotext-message.react.js +++ b/web/chat/robotext-message.react.js @@ -1,221 +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, 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, threadInfo, sidebarExistsOrCanBeCreated } = this.props; + const { item, sidebarExistsOrCanBeCreated } = this.props; const { id } = item.messageInfo; let messageTooltip; if ( this.props.mouseOverMessagePosition && this.props.mouseOverMessagePosition.item.messageInfo.id === id && sidebarExistsOrCanBeCreated ) { - messageTooltip = ( - - ); + 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 7c7385dde..60026aedc 100644 --- a/web/chat/tooltip-utils.js +++ b/web/chat/tooltip-utils.js @@ -1,146 +1,153 @@ // @flow import invariant from 'invariant'; +import * as React from 'react'; 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 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 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, +availablePositions: $ReadOnlyArray, +layoutPosition: 'relative' | 'absolute', }; function findTooltipPosition({ pointingToInfo, tooltipTexts, availablePositions, layoutPosition, }: FindTooltipPositionArgs): TooltipPosition { const { itemPosition: pointingTo, containerPosition } = pointingToInfo; 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; } 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 };