diff --git a/web/navigation-sidebar/account-settings-button.react.js b/web/navigation-sidebar/account-settings-button.react.js index 50a401cd4..0d79ad41f 100644 --- a/web/navigation-sidebar/account-settings-button.react.js +++ b/web/navigation-sidebar/account-settings-button.react.js @@ -1,43 +1,47 @@ // @flow import * as React from 'react'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import css from './account-settings-button.css'; +import { navigationSidebarLabelTooltipMargin } from './navigation-sidebar-constants.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; import { useNavigationSidebarTooltip } from '../utils/tooltip-action-utils.js'; +import { tooltipPositions } from '../utils/tooltip-utils.js'; function AccountSettingsButton(): React.Node { const dispatch = useDispatch(); const openAccountSettings = React.useCallback( () => dispatch({ type: updateNavInfoActionType, payload: { tab: 'settings', settingsSection: 'account', }, }), [dispatch], ); const { onMouseEnter, onMouseLeave } = useNavigationSidebarTooltip({ tooltipLabel: 'Settings', + position: tooltipPositions.RIGHT, + tooltipMargin: navigationSidebarLabelTooltipMargin, }); return (
); } export default AccountSettingsButton; diff --git a/web/navigation-sidebar/community-creation-button.react.js b/web/navigation-sidebar/community-creation-button.react.js index 8af5962a3..09be25d18 100644 --- a/web/navigation-sidebar/community-creation-button.react.js +++ b/web/navigation-sidebar/community-creation-button.react.js @@ -1,36 +1,40 @@ // @flow import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import css from './community-creation-button.css'; +import { navigationSidebarLabelTooltipMargin } from './navigation-sidebar-constants.js'; import CommunityCreationModal from '../sidebar/community-creation/community-creation-modal.react.js'; import { useNavigationSidebarTooltip } from '../utils/tooltip-action-utils.js'; +import { tooltipPositions } from '../utils/tooltip-utils.js'; function CommunityCreationButton(): React.Node { const { pushModal } = useModalContext(); const onPressCommunityCreationButton = React.useCallback( () => pushModal(), [pushModal], ); const { onMouseEnter, onMouseLeave } = useNavigationSidebarTooltip({ tooltipLabel: 'Create community', + position: tooltipPositions.RIGHT, + tooltipMargin: navigationSidebarLabelTooltipMargin, }); return (
); } export default CommunityCreationButton; diff --git a/web/navigation-sidebar/community-list-item.react.js b/web/navigation-sidebar/community-list-item.react.js index cafe6b995..8cdb38bd4 100644 --- a/web/navigation-sidebar/community-list-item.react.js +++ b/web/navigation-sidebar/community-list-item.react.js @@ -1,53 +1,57 @@ // @flow import * as React from 'react'; import { unreadCountSelectorForCommunity } from 'lib/selectors/thread-selectors.js'; import type { ResolvedThreadInfo } from 'lib/types/thread-types.js'; import css from './community-list-item.css'; +import { navigationSidebarLabelTooltipMargin } from './navigation-sidebar-constants.js'; import ThreadAvatar from '../avatars/thread-avatar.react.js'; import UnreadBadge from '../components/unread-badge.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useNavigationSidebarTooltip } from '../utils/tooltip-action-utils.js'; +import { tooltipPositions } from '../utils/tooltip-utils.js'; type Props = { +threadInfo: ResolvedThreadInfo, }; function CommunityListItem(props: Props): React.Node { const { threadInfo } = props; const { id: threadID } = threadInfo; const communityUnreadCountSelector = unreadCountSelectorForCommunity(threadID); const unreadCountValue = useSelector(communityUnreadCountSelector); const unreadBadge = React.useMemo(() => { if (unreadCountValue === 0) { return null; } return (
); }, [unreadCountValue]); const { onMouseEnter, onMouseLeave } = useNavigationSidebarTooltip({ tooltipLabel: threadInfo.uiName, + position: tooltipPositions.RIGHT, + tooltipMargin: navigationSidebarLabelTooltipMargin, }); return (
{unreadBadge}
); } export default CommunityListItem; diff --git a/web/navigation-sidebar/navigation-sidebar-constants.js b/web/navigation-sidebar/navigation-sidebar-constants.js index bf2270e53..3a5b89a14 100644 --- a/web/navigation-sidebar/navigation-sidebar-constants.js +++ b/web/navigation-sidebar/navigation-sidebar-constants.js @@ -1,13 +1,11 @@ // @flow -export const navigationSidebarTooltipContainerStyle = { - marginLeft: 24, -}; +export const navigationSidebarLabelTooltipMargin = 24; export const navigationSidebarTooltipStyle = { paddingTop: 8, paddingBottom: 8, paddingLeft: 8, paddingRight: 8, height: 17, }; diff --git a/web/navigation-sidebar/navigation-sidebar-home-button.react.js b/web/navigation-sidebar/navigation-sidebar-home-button.react.js index 93dc3bb83..c7220dd8c 100644 --- a/web/navigation-sidebar/navigation-sidebar-home-button.react.js +++ b/web/navigation-sidebar/navigation-sidebar-home-button.react.js @@ -1,45 +1,49 @@ // @flow import * as React from 'react'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { unreadCount } from 'lib/selectors/thread-selectors.js'; +import { navigationSidebarLabelTooltipMargin } from './navigation-sidebar-constants.js'; import css from './navigation-sidebar-home-button.css'; import UnreadBadge from '../components/unread-badge.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useNavigationSidebarTooltip } from '../utils/tooltip-action-utils.js'; +import { tooltipPositions } from '../utils/tooltip-utils.js'; function NavigationSidebarHomeButton(): React.Node { const unreadCountValue = useSelector(unreadCount); const { onMouseEnter, onMouseLeave } = useNavigationSidebarTooltip({ tooltipLabel: 'Home', + position: tooltipPositions.RIGHT, + tooltipMargin: navigationSidebarLabelTooltipMargin, }); const unreadBadge = React.useMemo(() => { if (unreadCountValue === 0) { return null; } return (
); }, [unreadCountValue]); return (
{unreadBadge}
); } export default NavigationSidebarHomeButton; diff --git a/web/navigation-sidebar/navigation-sidebar-toolitp.react.js b/web/navigation-sidebar/navigation-sidebar-toolitp.react.js index 32a1e46aa..1141dfb41 100644 --- a/web/navigation-sidebar/navigation-sidebar-toolitp.react.js +++ b/web/navigation-sidebar/navigation-sidebar-toolitp.react.js @@ -1,31 +1,49 @@ // @flow +import classNames from 'classnames'; import * as React from 'react'; -import { - navigationSidebarTooltipContainerStyle, - navigationSidebarTooltipStyle, -} from './navigation-sidebar-constants.js'; +import { navigationSidebarTooltipStyle } from './navigation-sidebar-constants.js'; import css from './navigation-sidebar-tooltip.css'; +import { + tooltipPositions, + type TooltipPosition, +} from '../utils/tooltip-utils.js'; type Props = { +tooltipLabel: string, + +position: TooltipPosition, + +tooltipMargin: number, }; function NavigationSidebarTooltip(props: Props): React.Node { - const { tooltipLabel } = props; + const { tooltipLabel, position, tooltipMargin } = props; + + const tooltipMarginStyle = React.useMemo( + () => ({ + marginLeft: position === tooltipPositions.RIGHT ? tooltipMargin : 0, + marginRight: position === tooltipPositions.LEFT ? tooltipMargin : 0, + marginTop: position === tooltipPositions.BOTTOM ? tooltipMargin : 0, + marginBottom: position === tooltipPositions.TOP ? tooltipMargin : 0, + }), + [position, tooltipMargin], + ); + + const arrowClassName = classNames(css.arrow, { + [css.arrowLeft]: position === tooltipPositions.RIGHT, + [css.arrowRight]: position === tooltipPositions.LEFT, + [css.arrowTop]: position === tooltipPositions.BOTTOM, + [css.arrowBottom]: position === tooltipPositions.TOP, + }); return ( -
-
+
+
{tooltipLabel}
); } export default NavigationSidebarTooltip; diff --git a/web/navigation-sidebar/navigation-sidebar-tooltip.css b/web/navigation-sidebar/navigation-sidebar-tooltip.css index d9a66a805..54a38fbad 100644 --- a/web/navigation-sidebar/navigation-sidebar-tooltip.css +++ b/web/navigation-sidebar/navigation-sidebar-tooltip.css @@ -1,23 +1,53 @@ .container { position: relative; } -.arrowLeft { +.arrow { position: absolute; - top: 50%; - transform: translateY(-50%); - right: 100%; width: 0; height: 0; +} + +.arrowLeft, +.arrowRight { + top: 50%; + transform: translateY(-50%); border-top: 10px solid transparent; border-bottom: 10px solid transparent; +} + +.arrowTop, +.arrowBottom { + left: 50%; + transform: translateX(-50%); + border-left: 10px solid transparent; + border-right: 10px solid transparent; +} + +.arrowLeft { + right: 100%; border-right: 10px solid var(--tooltip-background-primary-default); } +.arrowRight { + left: 100%; + border-left: 10px solid var(--tooltip-background-primary-default); +} + +.arrowTop { + bottom: 100%; + border-bottom: 10px solid var(--tooltip-background-primary-default); +} + +.arrowBottom { + top: 100%; + border-top: 10px solid var(--tooltip-background-primary-default); +} + .tooltipLabel { border-radius: 4px; background-color: var(--tooltip-background-primary-default); color: var(--tooltip-label-primary-default); font-size: var(--s-font-14); white-space: nowrap; } diff --git a/web/utils/tooltip-action-utils.js b/web/utils/tooltip-action-utils.js index 0b706fceb..a63ea6414 100644 --- a/web/utils/tooltip-action-utils.js +++ b/web/utils/tooltip-action-utils.js @@ -1,525 +1,539 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { useResettingState } from 'lib/hooks/use-resetting-state.js'; import type { ReactionInfo, ChatMessageInfoItem, } from 'lib/selectors/chat-selectors.js'; import { useCanEditMessage } from 'lib/shared/edit-messages-utils.js'; import { createMessageReply } from 'lib/shared/message-utils.js'; import { useCanCreateReactionFromMessage } from 'lib/shared/reaction-utils.js'; import { threadHasPermission, useSidebarExistsOrCanBeCreated, } from 'lib/shared/thread-utils.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { longAbsoluteDate } from 'lib/utils/date-utils.js'; import { canToggleMessagePin } from 'lib/utils/toggle-pin-utils.js'; import { type MessageTooltipAction, getTooltipPositionStyle, calculateMessageTooltipSize, calculateReactionTooltipSize, calculateNavigationSidebarTooltipSize, type TooltipPosition, type TooltipPositionStyle, type TooltipSize, } from './tooltip-utils.js'; -import { tooltipPositions } from './tooltip-utils.js'; import { getComposedMessageID } from '../chat/chat-constants.js'; import { useEditModalContext } from '../chat/edit-message-provider.js'; import MessageTooltip from '../chat/message-tooltip.react.js'; import type { PositionInfo } from '../chat/position-types.js'; import ReactionTooltip from '../chat/reaction-tooltip.react.js'; import { useTooltipContext } from '../chat/tooltip-provider.js'; import CommIcon from '../CommIcon.react.js'; import { InputStateContext } from '../input/input-state.js'; import TogglePinModal from '../modals/chat/toggle-pin-modal.react.js'; import NavigationSidebarTooltip from '../navigation-sidebar/navigation-sidebar-toolitp.react.js'; import { useOnClickPendingSidebar, useOnClickThread, } from '../selectors/thread-selectors.js'; type UseTooltipArgs = { +createTooltip: (tooltipPositionStyle: TooltipPositionStyle) => React.Node, +tooltipSize: TooltipSize, +availablePositions: $ReadOnlyArray, }; type UseTooltipResult = { +onMouseEnter: (event: SyntheticEvent) => mixed, +onMouseLeave: ?() => mixed, }; function useTooltip({ createTooltip, tooltipSize, availablePositions, }: UseTooltipArgs): UseTooltipResult { const [onMouseLeave, setOnMouseLeave] = React.useState mixed>(null); const [tooltipSourcePosition, setTooltipSourcePosition] = React.useState(); const { renderTooltip } = useTooltipContext(); const updateTooltip = React.useRef mixed>(); const onMouseEnter = React.useCallback( (event: SyntheticEvent) => { if (!renderTooltip) { return; } const rect = event.currentTarget.getBoundingClientRect(); const { top, bottom, left, right, height, width } = rect; const sourcePosition = { top, bottom, left, right, height, width }; setTooltipSourcePosition(sourcePosition); const tooltipPositionStyle = getTooltipPositionStyle({ tooltipSourcePosition: sourcePosition, tooltipSize, availablePositions, }); if (!tooltipPositionStyle) { return; } const tooltip = createTooltip(tooltipPositionStyle); const renderTooltipResult = renderTooltip({ newNode: tooltip, tooltipPositionStyle, }); if (renderTooltipResult) { const { onMouseLeaveCallback: callback } = renderTooltipResult; setOnMouseLeave((() => callback: () => () => mixed)); updateTooltip.current = renderTooltipResult.updateTooltip; } }, [availablePositions, createTooltip, renderTooltip, tooltipSize], ); React.useEffect(() => { if (!updateTooltip.current) { return; } const tooltipPositionStyle = getTooltipPositionStyle({ tooltipSourcePosition, tooltipSize, availablePositions, }); if (!tooltipPositionStyle) { return; } const tooltip = createTooltip(tooltipPositionStyle); updateTooltip.current?.(tooltip); }, [availablePositions, createTooltip, tooltipSize, tooltipSourcePosition]); return { onMouseEnter, onMouseLeave, }; } function useMessageTooltipSidebarAction( item: ChatMessageInfoItem, threadInfo: ThreadInfo, ): ?MessageTooltipAction { const { threadCreatedFromMessage, messageInfo } = item; const { popModal } = useModalContext(); const sidebarExists = !!threadCreatedFromMessage; const sidebarExistsOrCanBeCreated = useSidebarExistsOrCanBeCreated( threadInfo, item, ); const openThread = useOnClickThread(threadCreatedFromMessage); const openPendingSidebar = useOnClickPendingSidebar(messageInfo, threadInfo); return React.useMemo(() => { if (!sidebarExistsOrCanBeCreated) { return null; } const buttonContent = ; const onClick = (event: SyntheticEvent) => { popModal(); if (threadCreatedFromMessage) { openThread(event); } else { openPendingSidebar(event); } }; return { actionButtonContent: buttonContent, onClick, label: sidebarExists ? 'Go to thread' : 'Create thread', }; }, [ popModal, openPendingSidebar, openThread, sidebarExists, sidebarExistsOrCanBeCreated, threadCreatedFromMessage, ]); } function useMessageTooltipReplyAction( item: ChatMessageInfoItem, threadInfo: ThreadInfo, ): ?MessageTooltipAction { const { messageInfo } = item; const { popModal } = useModalContext(); const inputState = React.useContext(InputStateContext); invariant(inputState, 'inputState is required'); const { addReply } = inputState; return React.useMemo(() => { if ( item.messageInfo.type !== messageTypes.TEXT || !threadHasPermission(threadInfo, threadPermissions.VOICED) ) { return null; } const buttonContent = ; const onClick = () => { popModal(); if (!messageInfo.text) { return; } addReply(createMessageReply(messageInfo.text)); }; return { actionButtonContent: buttonContent, onClick, label: 'Reply', }; }, [popModal, addReply, item.messageInfo.type, messageInfo, threadInfo]); } const copiedMessageDurationMs = 2000; function useMessageCopyAction( item: ChatMessageInfoItem, ): ?MessageTooltipAction { const { messageInfo } = item; const [successful, setSuccessful] = useResettingState( false, copiedMessageDurationMs, ); return React.useMemo(() => { if (messageInfo.type !== messageTypes.TEXT) { return null; } const buttonContent = ; const onClick = async () => { try { await navigator.clipboard.writeText(messageInfo.text); setSuccessful(true); } catch (e) { setSuccessful(false); } }; return { actionButtonContent: buttonContent, onClick, label: successful ? 'Copied!' : 'Copy', }; }, [messageInfo.text, messageInfo.type, setSuccessful, successful]); } function useMessageReactAction( item: ChatMessageInfoItem, threadInfo: ThreadInfo, ): ?MessageTooltipAction { const { messageInfo } = item; const { setShouldRenderEmojiKeyboard } = useTooltipContext(); const canCreateReactionFromMessage = useCanCreateReactionFromMessage( threadInfo, messageInfo, ); return React.useMemo(() => { if (!canCreateReactionFromMessage) { return null; } const buttonContent = ; const onClickReact = () => { if (!setShouldRenderEmojiKeyboard) { return; } setShouldRenderEmojiKeyboard(true); }; return { actionButtonContent: buttonContent, onClick: onClickReact, label: 'React', }; }, [canCreateReactionFromMessage, setShouldRenderEmojiKeyboard]); } function useMessageTogglePinAction( item: ChatMessageInfoItem, threadInfo: ThreadInfo, ): ?MessageTooltipAction { const { pushModal } = useModalContext(); const { messageInfo, isPinned } = item; const canTogglePin = canToggleMessagePin(messageInfo, threadInfo); const inputState = React.useContext(InputStateContext); return React.useMemo(() => { if (!canTogglePin) { return null; } const iconName = isPinned ? 'unpin' : 'pin'; const buttonContent = ; const onClickTogglePin = () => { pushModal( , ); }; return { actionButtonContent: buttonContent, onClick: onClickTogglePin, label: isPinned ? 'Unpin' : 'Pin', }; }, [canTogglePin, inputState, isPinned, pushModal, item, threadInfo]); } function useMessageEditAction( item: ChatMessageInfoItem, threadInfo: ThreadInfo, ): ?MessageTooltipAction { const { messageInfo } = item; const canEditMessage = useCanEditMessage(threadInfo, messageInfo); const { renderEditModal, scrollToMessage } = useEditModalContext(); const { clearTooltip } = useTooltipContext(); return React.useMemo(() => { if (!canEditMessage) { return null; } const buttonContent = ; const onClickEdit = () => { const callback = (maxHeight: number) => renderEditModal({ messageInfo: item, threadInfo, isError: false, editedMessageDraft: messageInfo.text, maxHeight: maxHeight, }); clearTooltip(); scrollToMessage(getComposedMessageID(messageInfo), callback); }; return { actionButtonContent: buttonContent, onClick: onClickEdit, label: 'Edit', }; }, [ canEditMessage, clearTooltip, item, messageInfo, renderEditModal, scrollToMessage, threadInfo, ]); } function useMessageTooltipActions( item: ChatMessageInfoItem, threadInfo: ThreadInfo, ): $ReadOnlyArray { const sidebarAction = useMessageTooltipSidebarAction(item, threadInfo); const replyAction = useMessageTooltipReplyAction(item, threadInfo); const copyAction = useMessageCopyAction(item); const reactAction = useMessageReactAction(item, threadInfo); const togglePinAction = useMessageTogglePinAction(item, threadInfo); const editAction = useMessageEditAction(item, threadInfo); return React.useMemo( () => [ replyAction, sidebarAction, copyAction, reactAction, togglePinAction, editAction, ].filter(Boolean), [ replyAction, sidebarAction, copyAction, reactAction, togglePinAction, editAction, ], ); } const undefinedTooltipSize = { width: 0, height: 0, }; type UseMessageTooltipArgs = { +availablePositions: $ReadOnlyArray, +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, }; function useMessageTooltip({ availablePositions, item, threadInfo, }: UseMessageTooltipArgs): UseTooltipResult { const tooltipActions = useMessageTooltipActions(item, threadInfo); const messageTimestamp = React.useMemo(() => { const time = item.messageInfo.time; return longAbsoluteDate(time); }, [item.messageInfo.time]); const tooltipSize = React.useMemo(() => { if (typeof document === 'undefined') { return undefinedTooltipSize; } const tooltipLabels = tooltipActions.map(action => action.label); return calculateMessageTooltipSize({ tooltipLabels, timestamp: messageTimestamp, }); }, [messageTimestamp, tooltipActions]); const createMessageTooltip = React.useCallback( (tooltipPositionStyle: TooltipPositionStyle) => ( ), [item, messageTimestamp, threadInfo, tooltipActions, tooltipSize], ); const { onMouseEnter, onMouseLeave } = useTooltip({ createTooltip: createMessageTooltip, tooltipSize, availablePositions, }); return { onMouseEnter, onMouseLeave, }; } type UseReactionTooltipArgs = { +reaction: string, +reactions: ReactionInfo, +availablePositions: $ReadOnlyArray, }; function useReactionTooltip({ reaction, reactions, availablePositions, }: UseReactionTooltipArgs): UseTooltipResult { const { users } = reactions[reaction]; const tooltipSize = React.useMemo(() => { if (typeof document === 'undefined') { return undefinedTooltipSize; } const usernames = users.map(user => user.username).filter(Boolean); return calculateReactionTooltipSize(usernames); }, [users]); const createReactionTooltip = React.useCallback( () => , [reaction, reactions], ); const { onMouseEnter, onMouseLeave } = useTooltip({ createTooltip: createReactionTooltip, tooltipSize, availablePositions, }); return { onMouseEnter, onMouseLeave, }; } -const availableNavigationSidebarTooltipPositions = [tooltipPositions.RIGHT]; - type UseNavigationSidebarTooltipArgs = { +tooltipLabel: string, + +position: TooltipPosition, + // The margin size should be between the point of origin and + // the base of the tooltip. The arrow is a "decoration" and + // should not be considered when measuring the margin size. + +tooltipMargin: number, }; function useNavigationSidebarTooltip({ tooltipLabel, + position, + tooltipMargin, }: UseNavigationSidebarTooltipArgs): UseTooltipResult { const tooltipSize = React.useMemo(() => { if (typeof document === 'undefined') { return undefinedTooltipSize; } - return calculateNavigationSidebarTooltipSize(tooltipLabel); - }, [tooltipLabel]); + return calculateNavigationSidebarTooltipSize( + tooltipLabel, + position, + tooltipMargin, + ); + }, [position, tooltipLabel, tooltipMargin]); const createNavigationSidebarTooltip = React.useCallback( - () => , - [tooltipLabel], + () => ( + + ), + [position, tooltipLabel, tooltipMargin], ); const { onMouseEnter, onMouseLeave } = useTooltip({ createTooltip: createNavigationSidebarTooltip, tooltipSize, - availablePositions: availableNavigationSidebarTooltipPositions, + availablePositions: [position], }); return { onMouseEnter, onMouseLeave, }; } export { useMessageTooltipSidebarAction, useMessageTooltipReplyAction, useMessageReactAction, useMessageTooltipActions, useMessageTooltip, useReactionTooltip, useNavigationSidebarTooltip, }; diff --git a/web/utils/tooltip-utils.js b/web/utils/tooltip-utils.js index 47ea03d8f..c1fa14f57 100644 --- a/web/utils/tooltip-utils.js +++ b/web/utils/tooltip-utils.js @@ -1,443 +1,450 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { getAppContainerPositionInfo } from './window-utils.js'; import { tooltipButtonStyle, tooltipLabelStyle, tooltipStyle, reactionTooltipStyle, reactionSeeMoreLabel, } from '../chat/chat-constants.js'; import type { PositionInfo } from '../chat/position-types.js'; -import { - navigationSidebarTooltipContainerStyle, - navigationSidebarTooltipStyle, -} from '../navigation-sidebar/navigation-sidebar-constants.js'; +import { navigationSidebarTooltipStyle } from '../navigation-sidebar/navigation-sidebar-constants.js'; import { calculateMaxTextWidth } from '../utils/text-utils.js'; export const tooltipPositions = Object.freeze({ LEFT: 'left', RIGHT: 'right', LEFT_BOTTOM: 'left-bottom', RIGHT_BOTTOM: 'right-bottom', LEFT_TOP: 'left-top', RIGHT_TOP: 'right-top', TOP: 'top', BOTTOM: 'bottom', }); export type TooltipSize = { +height: number, +width: number, }; export type TooltipPositionStyle = { +anchorPoint: { +x: number, +y: 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, }; function getTooltipScreenOverflowRightCorrection( xAnchor: number, tooltipWidth: number, ): number { const appContainerPositionInfo = getAppContainerPositionInfo(); if (!appContainerPositionInfo) { return 0; } const { right: containerRight } = appContainerPositionInfo; const padding = 8; const tooltipRightEdge = xAnchor + tooltipWidth; const screenRightOverflow = tooltipRightEdge - containerRight; if (screenRightOverflow <= 0) { return 0; } return screenRightOverflow + padding; } type FindTooltipPositionArgs = { +sourcePositionInfo: PositionInfo, +tooltipSize: TooltipSize, +availablePositions: $ReadOnlyArray, +defaultPosition: TooltipPosition, }; function findTooltipPosition({ sourcePositionInfo, tooltipSize, availablePositions, defaultPosition, }: FindTooltipPositionArgs): TooltipPosition { const appContainerPositionInfo = getAppContainerPositionInfo(); if (!appContainerPositionInfo) { return defaultPosition; } const pointingTo = sourcePositionInfo; const { top: containerTop, left: containerLeft, 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 canBeDisplayedOnTopSideways = pointingTo.top >= containerTop && pointingTo.top + tooltipHeight <= containerBottom; const canBeDisplayedOnBottomSideways = pointingTo.bottom <= containerBottom && pointingTo.bottom - tooltipHeight >= containerTop; const verticalCenterOfPointingTo = pointingTo.top + pointingTo.height / 2; const horizontalCenterOfPointingTo = pointingTo.left + pointingTo.width / 2; const canBeDisplayedInTheMiddleSideways = verticalCenterOfPointingTo - tooltipHeight / 2 >= containerTop && verticalCenterOfPointingTo + tooltipHeight / 2 <= containerBottom; 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; for (const tooltipPosition of availablePositions) { if ( tooltipPosition === tooltipPositions.RIGHT && canBeDisplayedOnRight && canBeDisplayedInTheMiddleSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.RIGHT_BOTTOM && canBeDisplayedOnRight && canBeDisplayedOnBottomSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.LEFT && 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.RIGHT_TOP && canBeDisplayedOnRight && canBeDisplayedOnTopSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.TOP && canBeDisplayedOnTop ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.BOTTOM && canBeDisplayedOnBottom ) { return tooltipPosition; } } return defaultPosition; } type GetTooltipStyleParams = { +sourcePositionInfo: PositionInfo, +tooltipSize: TooltipSize, +tooltipPosition: TooltipPosition, }; // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return function getTooltipStyle({ sourcePositionInfo, tooltipSize, tooltipPosition, }: GetTooltipStyleParams): TooltipPositionStyle { if (tooltipPosition === tooltipPositions.RIGHT_TOP) { return { anchorPoint: { x: sourcePositionInfo.right, y: sourcePositionInfo.top, }, horizontalPosition: 'right', verticalPosition: 'bottom', alignment: 'left', }; } else if (tooltipPosition === tooltipPositions.LEFT_TOP) { return { anchorPoint: { x: sourcePositionInfo.left, y: sourcePositionInfo.top, }, horizontalPosition: 'left', verticalPosition: 'bottom', alignment: 'right', }; } else if (tooltipPosition === tooltipPositions.RIGHT_BOTTOM) { return { anchorPoint: { x: sourcePositionInfo.right, y: sourcePositionInfo.bottom, }, horizontalPosition: 'right', verticalPosition: 'top', alignment: 'left', }; } else if (tooltipPosition === tooltipPositions.LEFT_BOTTOM) { return { anchorPoint: { x: sourcePositionInfo.left, y: sourcePositionInfo.bottom, }, horizontalPosition: 'left', verticalPosition: 'top', alignment: 'right', }; } else if (tooltipPosition === tooltipPositions.LEFT) { return { anchorPoint: { x: sourcePositionInfo.left, y: sourcePositionInfo.top + sourcePositionInfo.height / 2 - tooltipSize.height / 2, }, horizontalPosition: 'left', verticalPosition: 'bottom', alignment: 'right', }; } else if (tooltipPosition === tooltipPositions.RIGHT) { return { anchorPoint: { x: sourcePositionInfo.right, y: sourcePositionInfo.top + sourcePositionInfo.height / 2 - tooltipSize.height / 2, }, horizontalPosition: 'right', verticalPosition: 'bottom', alignment: 'left', }; } else if (tooltipPosition === tooltipPositions.TOP) { const xAnchor = sourcePositionInfo.left + sourcePositionInfo.width / 2 - tooltipSize.width / 2; const tooltipOverflowRightCorrection = getTooltipScreenOverflowRightCorrection(xAnchor, tooltipSize.width); return { anchorPoint: { x: xAnchor - tooltipOverflowRightCorrection, y: sourcePositionInfo.top, }, horizontalPosition: 'right', verticalPosition: 'top', alignment: 'center', }; } else if (tooltipPosition === tooltipPositions.BOTTOM) { const xAnchor = sourcePositionInfo.left + sourcePositionInfo.width / 2 - tooltipSize.width / 2; const tooltipOverflowRightCorrection = getTooltipScreenOverflowRightCorrection(xAnchor, tooltipSize.width); return { anchorPoint: { x: xAnchor - tooltipOverflowRightCorrection, y: sourcePositionInfo.bottom, }, horizontalPosition: 'right', verticalPosition: 'bottom', alignment: 'center', }; } invariant(false, `Unexpected tooltip position value: ${tooltipPosition}`); } type GetTooltipPositionStyleParams = { +tooltipSourcePosition: ?PositionInfo, +tooltipSize: TooltipSize, +availablePositions: $ReadOnlyArray, }; function getTooltipPositionStyle( params: GetTooltipPositionStyleParams, ): ?TooltipPositionStyle { const { tooltipSourcePosition, tooltipSize, availablePositions } = params; if (!tooltipSourcePosition) { return undefined; } const tooltipPosition = findTooltipPosition({ sourcePositionInfo: tooltipSourcePosition, tooltipSize, availablePositions, defaultPosition: availablePositions[0], }); if (!tooltipPosition) { return undefined; } const tooltipPositionStyle = getTooltipStyle({ tooltipPosition, sourcePositionInfo: tooltipSourcePosition, tooltipSize, }); return tooltipPositionStyle; } type CalculateMessageTooltipSizeArgs = { +tooltipLabels: $ReadOnlyArray, +timestamp: string, }; function calculateMessageTooltipSize({ tooltipLabels, timestamp, }: CalculateMessageTooltipSizeArgs): TooltipSize { const textWidth = calculateMaxTextWidth([...tooltipLabels, timestamp], 14) + 2 * tooltipLabelStyle.padding; const buttonsWidth = tooltipLabels.length * (tooltipButtonStyle.width + tooltipButtonStyle.paddingLeft + tooltipButtonStyle.paddingRight); const width = Math.max(textWidth, buttonsWidth) + tooltipStyle.paddingLeft + tooltipStyle.paddingRight; const height = (tooltipLabelStyle.height + 2 * tooltipLabelStyle.padding) * 2 + tooltipStyle.rowGap * 2 + tooltipButtonStyle.height; return { width, height, }; } function calculateReactionTooltipSize( usernames: $ReadOnlyArray, ): TooltipSize { const showMoreTextIsShown = usernames.length > 5; const { maxWidth, maxHeight, paddingLeft, paddingRight, paddingTop, paddingBottom, rowGap, } = reactionTooltipStyle; const maxTooltipContentWidth = maxWidth; const maxTooltipContentHeight = maxHeight; const usernamesTextWidth = calculateMaxTextWidth(usernames, 14); const seeMoreTextWidth = calculateMaxTextWidth([reactionSeeMoreLabel], 12); let textWidth = usernamesTextWidth; if (showMoreTextIsShown) { textWidth = Math.max(usernamesTextWidth, seeMoreTextWidth); } const width = Math.min(maxTooltipContentWidth, textWidth) + paddingLeft + paddingRight; let height = usernames.length * tooltipLabelStyle.height + (usernames.length - 1) * rowGap; if (showMoreTextIsShown) { height = maxTooltipContentHeight; } height += paddingTop + paddingBottom; return { width, height, }; } function calculateNavigationSidebarTooltipSize( tooltipLabel: string, + position: TooltipPosition, + tooltipMargin: number, ): TooltipSize { - const { marginLeft } = navigationSidebarTooltipContainerStyle; const { paddingLeft, paddingRight, paddingTop, paddingBottom, height: contentHeight, } = navigationSidebarTooltipStyle; const tooltipLabelTextWidth = calculateMaxTextWidth([tooltipLabel], 14); - const width = marginLeft + paddingLeft + tooltipLabelTextWidth + paddingRight; - const height = paddingTop + contentHeight + paddingBottom; + const marginIsHorizontal = + position === tooltipPositions.RIGHT || position === tooltipPositions.LEFT; + const marginIsVertical = + position === tooltipPositions.TOP || position === tooltipPositions.BOTTOM; + + const horizontalMargin = marginIsHorizontal ? tooltipMargin : 0; + const verticalMargin = marginIsVertical ? tooltipMargin : 0; + + const width = + paddingLeft + tooltipLabelTextWidth + paddingRight + horizontalMargin; + const height = paddingTop + contentHeight + paddingBottom + verticalMargin; return { width, height, }; } export { findTooltipPosition, getTooltipPositionStyle, calculateMessageTooltipSize, calculateReactionTooltipSize, calculateNavigationSidebarTooltipSize, };