Skip to content

Commit c99c1cc

Browse files
committed
feat(TitleTooltip): Stick tooltip to cursor if no side is defined
1 parent 5ad6897 commit c99c1cc

13 files changed

Lines changed: 177 additions & 88 deletions

File tree

bun.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"analyze": "bunx vite-bundle-analyzer"
2222
},
2323
"dependencies": {
24+
"@floating-ui/react": "0.27.18",
2425
"@lingui/core": "5.9.1",
2526
"@lingui/react": "5.9.1",
2627
"@radix-ui/react-checkbox": "1.3.3",

src/app/providers.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ import { useHashLocation } from "wouter/use-hash-location"
55

66
import { DialogProvider } from "components/ui/dialog"
77
import { Toaster } from "components/ui/toaster"
8-
import { Tooltip } from "components/ui/tooltip"
8+
import { TooltipProvider } from "components/ui/tooltip"
99
import { LocaleProvider } from "locales/locale-provider"
1010

1111
export const AppProviders = ({ children }: PropsWithChildren) => (
1212
<LocaleProvider>
1313
<Router hook={useHashLocation}>
14-
<Tooltip.Provider>
14+
<TooltipProvider>
1515
<Toaster />
1616
<DialogProvider />
1717
{children}
18-
</Tooltip.Provider>
18+
</TooltipProvider>
1919
</Router>
2020
</LocaleProvider>
2121
)

src/app/routes/calendar/calendar-route.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,7 @@ const DayColumn = ({
115115
{category?.fullName || t`No category`}
116116
</span>
117117
) : (
118-
<TitleTooltip
119-
asChild
120-
title={category?.fullName || t`No category`}
121-
side="top"
122-
>
118+
<TitleTooltip title={category?.fullName || t`No category`}>
123119
<span className="absolute inset-0" />
124120
</TitleTooltip>
125121
)}

src/components/ui/file-input/file-input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const FileInput = ({
5555
}
5656

5757
return (
58-
<TitleTooltip title={alert?.text} side="bottom" asChild>
58+
<TitleTooltip title={alert?.text}>
5959
<div
6060
className="inline-block"
6161
onDrop={handleDrop}

src/components/ui/icon-button/icon-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const IconButton = ({
4747
iconSize = "sm",
4848
...delegated
4949
}: IconButtonProps) => (
50-
<TitleTooltip title={hideTitle ? undefined : title} side={titleSide} asChild>
50+
<TitleTooltip title={hideTitle ? undefined : title} side={titleSide}>
5151
<Button
5252
ref={ref}
5353
look={look}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { PropsWithChildren, useState } from "react"
2+
3+
import {
4+
FloatingPortal,
5+
offset,
6+
shift,
7+
useClientPoint,
8+
useDismiss,
9+
useFloating,
10+
useFocus,
11+
useHover,
12+
useInteractions,
13+
useRole,
14+
useTransitionStyles,
15+
} from "@floating-ui/react"
16+
17+
import { Slot } from "components/utility/slot"
18+
import { ClassNameProp, TitleProp } from "types/base-props"
19+
import { cn } from "utils/cn"
20+
import { zIndex } from "utils/z-index"
21+
22+
import { tooltipStyles } from "./tooltip-styles"
23+
24+
interface CursorTooltipProps extends Required<TitleProp>, ClassNameProp {}
25+
26+
export const CursorTooltip = ({
27+
title,
28+
children,
29+
className,
30+
}: PropsWithChildren<CursorTooltipProps>) => {
31+
const [open, setOpen] = useState(false)
32+
33+
const { refs, floatingStyles, context } = useFloating({
34+
open,
35+
onOpenChange: setOpen,
36+
middleware: [offset({ mainAxis: 16, crossAxis: 8 }), shift({ padding: 8 })],
37+
placement: "bottom-start",
38+
})
39+
40+
const { isMounted, styles } = useTransitionStyles(context, {
41+
initial: { opacity: 0, zoom: 0.75 },
42+
open: { opacity: 1, zoom: 1 },
43+
close: { opacity: 0, zoom: 0.75 },
44+
duration: 150,
45+
})
46+
47+
const { getReferenceProps, getFloatingProps } = useInteractions([
48+
useClientPoint(context),
49+
useHover(context),
50+
useFocus(context),
51+
useDismiss(context),
52+
useRole(context, { role: "tooltip" }),
53+
])
54+
55+
return (
56+
<>
57+
<Slot ref={refs.setReference} {...getReferenceProps()}>
58+
{children}
59+
</Slot>
60+
61+
{isMounted && (
62+
<FloatingPortal>
63+
<div
64+
// eslint-disable-next-line react-hooks/refs -- false positive
65+
ref={refs.setFloating}
66+
style={floatingStyles}
67+
className={zIndex.tooltip}
68+
{...getFloatingProps()}
69+
>
70+
<div style={styles} className={cn(tooltipStyles, className)}>
71+
{title}
72+
</div>
73+
</div>
74+
</FloatingPortal>
75+
)}
76+
</>
77+
)
78+
}

src/components/ui/tooltip/title-tooltip.tsx

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,28 @@ import { PropsWithChildren } from "react"
22

33
import { TooltipContentProps } from "@radix-ui/react-tooltip"
44

5-
import { AsChildProp, ClassNameProp, TitleProp } from "types/base-props"
5+
import { ClassNameProp, TitleProp } from "types/base-props"
66

7+
import { CursorTooltip } from "./cursor-tooltip"
78
import { Tooltip } from "./tooltip"
89

9-
export interface TitleTooltipProps
10-
extends TitleProp, AsChildProp, ClassNameProp {
10+
export interface TitleTooltipProps extends TitleProp, ClassNameProp {
1111
side?: TooltipContentProps["side"]
12-
force?: boolean
1312
}
1413
export const TitleTooltip = ({
1514
title,
16-
asChild,
1715
side,
1816
children,
19-
force,
2017
className,
2118
}: PropsWithChildren<TitleTooltipProps>) =>
2219
!title ? (
2320
children
21+
) : !side ? (
22+
<CursorTooltip className={className} title={title}>
23+
{children}
24+
</CursorTooltip>
2425
) : (
25-
<Tooltip.Root open={force}>
26-
<Tooltip.Trigger asChild={asChild} className={className}>
27-
{children}
28-
</Tooltip.Trigger>
29-
<Tooltip.Portal>
30-
<Tooltip.Content side={side}>{title}</Tooltip.Content>
31-
</Tooltip.Portal>
32-
</Tooltip.Root>
26+
<Tooltip className={className} content={title} side={side}>
27+
{children}
28+
</Tooltip>
3329
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { cn } from "utils/cn"
2+
import { surface } from "utils/styles"
3+
import { zIndex } from "utils/z-index"
4+
5+
export const tooltipStyles = cn(
6+
surface({ look: "overlay", size: "md" }),
7+
"pointer-events-none overflow-hidden px-3 py-1.5 text-sm",
8+
zIndex.tooltip
9+
)
Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,44 @@
1-
import { ComponentProps } from "react"
1+
import { PropsWithChildren, ReactNode } from "react"
22

33
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
44

5+
import { ClassNameProp } from "types/base-props"
56
import { cn } from "utils/cn"
6-
import { surface } from "utils/styles"
7-
import { zIndex } from "utils/z-index"
87

9-
const Content = ({
10-
ref,
11-
className,
12-
sideOffset = 4,
13-
...props
14-
}: ComponentProps<typeof TooltipPrimitive.Content>) => (
15-
<TooltipPrimitive.Content
16-
ref={ref}
17-
sideOffset={sideOffset}
18-
className={cn(
19-
surface({ look: "overlay", size: "md" }),
20-
"animate-in fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
21-
"overflow-hidden px-3 py-1.5 text-sm",
22-
zIndex.tooltip,
23-
className
24-
)}
25-
{...props}
26-
/>
27-
)
8+
import { tooltipStyles } from "./tooltip-styles"
289

29-
const { Root, Trigger, Portal, Provider } = TooltipPrimitive
10+
export const TooltipProvider = TooltipPrimitive.Provider
3011

31-
export const Tooltip = {
32-
Root,
33-
Trigger,
34-
Content,
35-
Portal,
36-
Provider,
12+
interface TooltipProps extends ClassNameProp {
13+
content: ReactNode
14+
side?: TooltipPrimitive.TooltipContentProps["side"]
15+
align?: TooltipPrimitive.TooltipContentProps["align"]
3716
}
17+
export const Tooltip = ({
18+
content,
19+
side,
20+
align,
21+
children,
22+
className,
23+
}: PropsWithChildren<TooltipProps>) => (
24+
<TooltipPrimitive.Root>
25+
<TooltipPrimitive.Trigger asChild className={className}>
26+
{children}
27+
</TooltipPrimitive.Trigger>
28+
29+
<TooltipPrimitive.Portal>
30+
<TooltipPrimitive.Content
31+
side={side}
32+
align={align}
33+
sideOffset={4}
34+
className={cn(
35+
tooltipStyles,
36+
"animate-in fade-in-0 zoom-in-75 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-75",
37+
className
38+
)}
39+
>
40+
{content}
41+
</TooltipPrimitive.Content>
42+
</TooltipPrimitive.Portal>
43+
</TooltipPrimitive.Root>
44+
)

0 commit comments

Comments
 (0)