Skip to content

Commit

Permalink
[CP-3146] Implemented Tooltip Flipping to Maintain Viewport Visibility (
Browse files Browse the repository at this point in the history
  • Loading branch information
dkarski authored Oct 3, 2024
1 parent 84e6507 commit b7c8545
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 23 deletions.
9 changes: 7 additions & 2 deletions libs/device/models/src/lib/feature/extra-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@

import { z } from "zod"

const TooltipPlacementEnum = z.enum(["bottom-right", "bottom-left"]);
const TooltipPlacementEnum = z.enum([
"bottom-right",
"bottom-left",
"top-right",
"top-left",
])

export type TooltipPlacement = z.infer<typeof TooltipPlacementEnum>;
export type TooltipPlacement = z.infer<typeof TooltipPlacementEnum>

const tooltipSchema = z.object({
contentText: z.string().optional(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/

import { TooltipPlacement } from "device/models"

export const flipVertical = (placement: TooltipPlacement): TooltipPlacement => {
return placement.startsWith("bottom")
? (`top-${placement.split("-")[1]}` as TooltipPlacement)
: (`bottom-${placement.split("-")[1]}` as TooltipPlacement)
}

export const flipHorizontal = (
placement: TooltipPlacement
): TooltipPlacement => {
return placement.endsWith("right")
? (`-${placement.replace("right", "left")}` as TooltipPlacement)
: (`-${placement.replace("left", "right")}` as TooltipPlacement)
}

export const flipTooltipPlacement = (
placement: TooltipPlacement
): TooltipPlacement => {
const [vertical, horizontal] = placement.split("-")
const flippedVertical = vertical === "bottom" ? "top" : "bottom"
const flippedHorizontal = horizontal === "right" ? "left" : "right"
return `${flippedVertical}-${flippedHorizontal}` as TooltipPlacement
}
96 changes: 75 additions & 21 deletions libs/generic-view/ui/src/lib/interactive/tooltip/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ import React, {
import styled, { css } from "styled-components"
import { BaseGenericComponent } from "generic-view/utils"
import { TooltipPlacement } from "device/models"
import {
flipHorizontal,
flipTooltipPlacement,
flipVertical,
} from "./tooltip-helpers"

interface Position {
left: number
top: number
}

export const Tooltip: BaseGenericComponent<
undefined,
Expand All @@ -24,33 +34,75 @@ export const Tooltip: BaseGenericComponent<
Anchor: typeof TooltipAnchor
Content: typeof TooltipContent
} = ({ children, placement = "bottom-right" }) => {
const [anchorPosition, setAnchorPosition] = useState<{
top?: number
left?: number
}>({})
const [contentPosition, setContentPosition] = useState<Partial<Position>>({})

const contentRef = useRef<HTMLDivElement>(null)

const handleAnchorHover = useCallback(
(event: MouseEvent) => {
const anchorRect = event.currentTarget.getBoundingClientRect()
const contentReact = contentRef.current?.getBoundingClientRect()

if (contentReact === undefined) {
return
const contentRect = contentRef.current?.getBoundingClientRect()

if (!contentRect) return

const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight

const placements: TooltipPlacement[] = [
placement,
flipVertical(placement),
flipHorizontal(placement),
flipTooltipPlacement(placement),
]

const calculatePosition = (placment: TooltipPlacement): Position => {
let top = 0
let left = 0

switch (placment) {
case "bottom-right":
top = anchorRect.top + anchorRect.height
left = anchorRect.left
break
case "bottom-left":
top = anchorRect.top + anchorRect.height
left = anchorRect.left - contentRect.width + anchorRect.width
break
case "top-right":
top = anchorRect.top - contentRect.height - anchorRect.height
left = anchorRect.left
break
case "top-left":
top = anchorRect.top - contentRect.height - anchorRect.height
left = anchorRect.left - contentRect.width + anchorRect.width
break
default:
top = anchorRect.top + anchorRect.height
left = anchorRect.left
}

return { top, left }
}

if (placement === "bottom-right") {
const top = anchorRect.top + anchorRect.height
const left = anchorRect.left

setAnchorPosition({ left, top })
} else if (placement === "bottom-left") {
const top = anchorRect.top + anchorRect.height
const left = anchorRect.left - contentReact.width + anchorRect.width
const isWithinViewport = (position: Position): boolean => {
return (
position.left >= 0 &&
position.top >= 0 &&
position.left + contentRect.width <= viewportWidth &&
position.top + contentRect.height <= viewportHeight
)
}

setAnchorPosition({ left, top })
for (const placement of placements) {
const position = calculatePosition(placement)
if (isWithinViewport(position)) {
setContentPosition(position)
return
}
}

const position = calculatePosition(placement)
setContentPosition(position)
},
[placement]
)
Expand Down Expand Up @@ -82,13 +134,13 @@ export const Tooltip: BaseGenericComponent<
}
return React.cloneElement(child as ReactElement, {
...child.props,
$top: anchorPosition.top,
$left: anchorPosition.left,
$top: contentPosition.top,
$left: contentPosition.left,
ref: contentRef,
$placement: placement,
})
})
}, [children, anchorPosition.top, anchorPosition.left, placement])
}, [children, contentPosition.top, contentPosition.left, placement])

return (
<Container>
Expand Down Expand Up @@ -171,7 +223,9 @@ const Content = styled.div<{
width: 100%;
color: ${theme.color.black};
white-space: pre-wrap;
text-align: ${$placement === "bottom-left" ? "right" : "left"};
text-align: ${$placement === "bottom-left" || $placement === "top-left"
? "right"
: "left"};
}
`}
`
Expand Down

0 comments on commit b7c8545

Please sign in to comment.