diff --git a/web/chat/tooltip-provider.js b/web/chat/tooltip-provider.js new file mode 100644 index 000000000..973e094be --- /dev/null +++ b/web/chat/tooltip-provider.js @@ -0,0 +1,90 @@ +// @flow + +import invariant from 'invariant'; +import * as React from 'react'; + +import type { TooltipPositionStyle } from './tooltip-utils'; + +export type RenderTooltipParams = { + +newNode: React.Node, + +tooltipPositionStyle: TooltipPositionStyle, +}; + +export type RenderTooltipResult = { + +onMouseLeaveCallback: () => mixed, + +clearTooltip: () => mixed, +}; + +type TooltipContextType = { + +renderTooltip: (params: RenderTooltipParams) => RenderTooltipResult, + +clearTooltip: () => mixed, +}; + +const TooltipContext: React.Context = React.createContext( + { + renderTooltip: () => ({ + onMouseLeaveCallback: () => {}, + clearTooltip: () => {}, + }), + clearTooltip: () => {}, + }, +); + +type Props = { + +children: React.Node, +}; +function TooltipProvider(props: Props): React.Node { + const { children } = props; + // eslint-disable-next-line no-unused-vars + const tooltipSymbol = React.useRef(null); + // eslint-disable-next-line no-unused-vars + const tooltipCancelTimer = React.useRef(null); + + // eslint-disable-next-line no-unused-vars + const [tooltipNode, setTooltipNode] = React.useState(null); + const [ + // eslint-disable-next-line no-unused-vars + tooltipPosition, + // eslint-disable-next-line no-unused-vars + setTooltipPosition, + ] = React.useState(null); + + const clearCurrentTooltip = React.useCallback(() => {}, []); + + const renderTooltip = React.useCallback( + () => ({ onMouseLeaveCallback: () => {}, clearTooltip: () => {} }), + [], + ); + + // eslint-disable-next-line no-unused-vars + const onMouseEnterTooltip = React.useCallback(() => {}, []); + + // eslint-disable-next-line no-unused-vars + const onMouseLeaveTooltip = React.useCallback(() => {}, []); + + const tooltip = React.useMemo(() => {}, []); + + const value = React.useMemo( + () => ({ + renderTooltip, + clearTooltip: clearCurrentTooltip, + }), + [renderTooltip, clearCurrentTooltip], + ); + + return ( + + {children} + {tooltip} + + ); +} + +function useTooltipContext(): TooltipContextType { + const context = React.useContext(TooltipContext); + invariant(context, 'TooltipContext not found'); + + return context; +} + +export { TooltipProvider, useTooltipContext }; diff --git a/web/chat/tooltip-utils.js b/web/chat/tooltip-utils.js index 30220dccd..7c7385dde 100644 --- a/web/chat/tooltip-utils.js +++ b/web/chat/tooltip-utils.js @@ -1,137 +1,146 @@ // @flow 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 TooltipPositionStyle = { + +xCoord: number, + +yCoord: number, + +verticalPosition: 'top' | 'bottom', + +horizontalPosition: 'left' | 'right', + +alignment: 'left' | 'center' | 'right', +}; + export type TooltipPosition = $Values; 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 };