Skip to content

Commit bcb2fbb

Browse files
gabrieljablonskidanielbarion
authored andcommitted
add provider support for multiple tooltips
1 parent 37430ea commit bcb2fbb

File tree

4 files changed

+100
-40
lines changed

4 files changed

+100
-40
lines changed

src/App.tsx

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,25 @@ import { useEffect, useRef, useState } from 'react'
44
import styles from './styles.module.css'
55

66
function WithProvider() {
7-
const { attach, detach } = useTooltip()
7+
const tooltipper = useTooltip()
8+
const { attach, detach } = tooltipper()
9+
const { attach: attach1, detach: detach1 } = tooltipper('tooltip-1')
10+
const { attach: attach2, detach: detach2 } = tooltipper('tooltip-2')
811
const buttonRef1 = useRef<HTMLButtonElement>(null)
912
const buttonRef2 = useRef<HTMLButtonElement>(null)
10-
const [clickCount, setClickCount] = useState(0)
13+
const buttonRef3 = useRef<HTMLButtonElement>(null)
14+
const buttonRef4 = useRef<HTMLButtonElement>(null)
15+
const buttonRef5 = useRef<HTMLButtonElement>(null)
16+
const buttonRef6 = useRef<HTMLButtonElement>(null)
1117

1218
useEffect(() => {
1319
attach(buttonRef1, buttonRef2)
20+
attach1(buttonRef3, buttonRef4)
21+
attach2(buttonRef5, buttonRef6)
1422
return () => {
1523
detach(buttonRef1, buttonRef2)
24+
detach1(buttonRef3, buttonRef4)
25+
detach2(buttonRef5, buttonRef6)
1626
}
1727
}, [])
1828

@@ -22,20 +32,33 @@ function WithProvider() {
2232
<button
2333
ref={buttonRef1}
2434
data-tooltip-place="right"
25-
data-tooltip-content={`Hello World from a Tooltip ${clickCount}`}
26-
onClick={() => setClickCount((i) => i + 1)}
27-
>
28-
Hover or focus me 4
29-
</button>
30-
<button
31-
ref={buttonRef2}
32-
data-tooltip-place="bottom"
33-
data-tooltip-content="Hello World from a Tooltip 5"
35+
data-tooltip-content="Hello World from a Shared Global Tooltip"
3436
>
3537
Hover or focus me 5
3638
</button>
39+
<button ref={buttonRef2} data-tooltip-content="Hello World from a Shared Global Tooltip">
40+
Hover or focus me 6
41+
</button>
42+
</p>
43+
<p>
44+
<button ref={buttonRef3} data-tooltip-content="Hello World from Shared Tooltip 1">
45+
Hover or focus me 7
46+
</button>
47+
<button ref={buttonRef4} data-tooltip-content="Hello World from Shared Tooltip 1">
48+
Hover or focus me 8
49+
</button>
50+
</p>
51+
<p>
52+
<button ref={buttonRef5} data-tooltip-content="Hello World from Shared Tooltip 2">
53+
Hover or focus me 9
54+
</button>
55+
<button ref={buttonRef6} data-tooltip-content="Hello World from Shared Tooltip 2">
56+
Hover or focus me 10
57+
</button>
3758
</p>
3859
<Tooltip />
60+
<Tooltip id="tooltip-1" />
61+
<Tooltip id="tooltip-2" />
3962
</section>
4063
)
4164
}

src/components/Tooltip/Tooltip.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState, useId, useRef } from 'react'
1+
import { useEffect, useState, useRef } from 'react'
22
import classNames from 'classnames'
33
import debounce from 'utils/debounce'
44
import { TooltipContent } from 'components/TooltipContent'
@@ -9,7 +9,7 @@ import type { ITooltip } from './TooltipTypes'
99

1010
const Tooltip = ({
1111
// props
12-
id = useId(),
12+
id,
1313
className,
1414
classNameArrow,
1515
variant = 'dark',
@@ -36,7 +36,7 @@ const Tooltip = ({
3636
const [inlineStyles, setInlineStyles] = useState({})
3737
const [inlineArrowStyles, setInlineArrowStyles] = useState({})
3838
const [show, setShow] = useState<boolean>(false)
39-
const { anchorRefs, setActiveAnchor: setProviderActiveAnchor } = useTooltip()
39+
const { anchorRefs, setActiveAnchor: setProviderActiveAnchor } = useTooltip()(id)
4040
const [activeAnchor, setActiveAnchor] = useState<React.RefObject<HTMLElement>>({ current: null })
4141

4242
const handleShow = (value: boolean) => {

src/components/TooltipController/TooltipController.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const TooltipController = ({
4141
const [tooltipEvents, setTooltipEvents] = useState(events)
4242
const [tooltipPositionStrategy, setTooltipPositionStrategy] = useState(positionStrategy)
4343
const [isHtmlContent, setIsHtmlContent] = useState(Boolean(html))
44-
const { anchorRefs, activeAnchor } = useTooltip()
44+
const { anchorRefs, activeAnchor } = useTooltip()(id)
4545

4646
const getDataAttributesFromAnchorElement = (elementReference: HTMLElement) => {
4747
const dataAttributes = elementReference?.getAttributeNames().reduce((acc, name) => {

src/components/TooltipProvider/TooltipProvider.tsx

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,87 @@
1-
import React, { createContext, PropsWithChildren, useContext, useMemo, useState } from 'react'
1+
import React, {
2+
createContext,
3+
PropsWithChildren,
4+
useCallback,
5+
useContext,
6+
useId,
7+
useState,
8+
} from 'react'
9+
10+
type AnchorRef = React.RefObject<HTMLElement>
211

312
interface TooltipContextData {
4-
anchorRefs: Set<React.RefObject<HTMLElement>>
5-
attach: (...ref: React.RefObject<HTMLElement>[]) => void
6-
detach: (...ref: React.RefObject<HTMLElement>[]) => void
7-
activeAnchor: React.RefObject<HTMLElement>
8-
setActiveAnchor: React.Dispatch<React.SetStateAction<React.RefObject<HTMLElement>>>
13+
// when using one tooltip
14+
anchorRefs: Set<AnchorRef>
15+
activeAnchor: AnchorRef
16+
attach: (...refs: AnchorRef[]) => void
17+
detach: (...refs: AnchorRef[]) => void
18+
setActiveAnchor: (ref: AnchorRef) => void
919
}
1020

11-
const TooltipContext = createContext<TooltipContextData>({
21+
type TooltipContextDataBuilder = (tooltipId?: string) => TooltipContextData
22+
23+
const TooltipContext = createContext<TooltipContextDataBuilder>(() => ({
1224
anchorRefs: new Set(),
25+
activeAnchor: { current: null },
1326
attach: () => {
1427
/* attach anchor element */
1528
},
1629
detach: () => {
1730
/* detach anchor element */
1831
},
19-
activeAnchor: { current: null },
2032
setActiveAnchor: () => {
2133
/* set active anchor */
2234
},
23-
})
35+
}))
2436

2537
const TooltipProvider: React.FC<PropsWithChildren> = ({ children }) => {
26-
const [anchorRefs, setAnchorRefs] = useState<Set<React.RefObject<HTMLElement>>>(new Set())
27-
const [activeAnchor, setActiveAnchor] = useState<React.RefObject<HTMLElement>>({ current: null })
28-
29-
const attach = (...refs: React.RefObject<HTMLElement>[]) => {
30-
setAnchorRefs((oldRefs) => {
31-
refs.forEach((ref) => oldRefs.add(ref))
32-
// create new set to trigger re-render
33-
return new Set(oldRefs)
38+
const defaultTooltipId = useId()
39+
const [anchorRefMap, setAnchorRefMap] = useState<Record<string, Set<AnchorRef>>>({
40+
[defaultTooltipId]: new Set(),
41+
})
42+
const [activeAnchorMap, setActiveAnchorMap] = useState<Record<string, AnchorRef>>({
43+
[defaultTooltipId]: { current: null },
44+
})
45+
46+
const attach = (tooltipId: string, ...refs: AnchorRef[]) => {
47+
setAnchorRefMap((oldMap) => {
48+
const tooltipRefs = oldMap[tooltipId] ?? new Set()
49+
refs.forEach((ref) => tooltipRefs.add(ref))
50+
// create new object to trigger re-render
51+
return { ...oldMap, [tooltipId]: tooltipRefs }
52+
})
53+
}
54+
55+
const detach = (tooltipId: string, ...refs: AnchorRef[]) => {
56+
setAnchorRefMap((oldMap) => {
57+
const tooltipRefs = oldMap[tooltipId]
58+
if (!tooltipRefs) {
59+
// tooltip not found
60+
// maybe thow error?
61+
return oldMap
62+
}
63+
refs.forEach((ref) => tooltipRefs.delete(ref))
64+
// create new object to trigger re-render
65+
return { ...oldMap }
3466
})
3567
}
3668

37-
const detach = (...refs: React.RefObject<HTMLElement>[]) => {
38-
setAnchorRefs((oldRefs) => {
39-
refs.forEach((ref) => oldRefs.delete(ref))
40-
// create new set to trigger re-render
41-
return new Set(oldRefs)
69+
const setActiveAnchor = (tooltipId: string, ref: React.RefObject<HTMLElement>) => {
70+
setActiveAnchorMap((oldMap) => {
71+
// create new object to trigger re-render
72+
return { ...oldMap, [tooltipId]: ref }
4273
})
4374
}
4475

45-
const context = useMemo(
46-
() => ({ anchorRefs, attach, detach, activeAnchor, setActiveAnchor }),
47-
[anchorRefs, attach, detach, activeAnchor, setActiveAnchor],
76+
const context = useCallback(
77+
(tooltipId?: string) => ({
78+
anchorRefs: anchorRefMap[tooltipId ?? defaultTooltipId] ?? new Set(),
79+
activeAnchor: activeAnchorMap[tooltipId ?? defaultTooltipId] ?? { current: null },
80+
attach: (...refs: AnchorRef[]) => attach(tooltipId ?? defaultTooltipId, ...refs),
81+
detach: (...refs: AnchorRef[]) => detach(tooltipId ?? defaultTooltipId, ...refs),
82+
setActiveAnchor: (ref: AnchorRef) => setActiveAnchor(tooltipId ?? defaultTooltipId, ref),
83+
}),
84+
[defaultTooltipId, anchorRefMap, activeAnchorMap, attach, detach],
4885
)
4986

5087
return <TooltipContext.Provider value={context}>{children}</TooltipContext.Provider>

0 commit comments

Comments
 (0)