react-tooltip 5.1.0 → 5.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/react-tooltip.cjs.js +1 -1
- package/dist/react-tooltip.cjs.min.js +1 -1
- package/dist/react-tooltip.d.ts +2 -3
- package/dist/react-tooltip.esm.js +1 -1
- package/dist/react-tooltip.esm.min.js +1 -1
- package/dist/react-tooltip.umd.js +1 -1
- package/dist/react-tooltip.umd.min.js +1 -1
- package/package.json +1 -1
- package/src/App.tsx +119 -0
- package/src/components/Tooltip/Tooltip.tsx +226 -0
- package/src/components/Tooltip/TooltipTypes.d.ts +47 -0
- package/src/components/Tooltip/index.ts +1 -0
- package/src/components/Tooltip/styles.module.css +62 -0
- package/src/components/TooltipContent/TooltipContent.tsx +8 -0
- package/src/components/TooltipContent/TooltipContentTypes.d.ts +3 -0
- package/src/components/TooltipContent/index.ts +1 -0
- package/src/components/TooltipController/TooltipController.tsx +187 -0
- package/src/components/TooltipController/TooltipControllerTypes.d.ts +46 -0
- package/src/components/TooltipController/index.ts +1 -0
- package/src/components/TooltipProvider/TooltipProvider.tsx +110 -0
- package/src/components/TooltipProvider/TooltipProviderTypes.d.ts +33 -0
- package/src/components/TooltipProvider/TooltipWrapper.tsx +48 -0
- package/src/components/TooltipProvider/index.ts +2 -0
- package/src/index-dev.tsx +16 -0
- package/src/index.tsx +4 -0
- package/src/styles.module.css +5 -0
- package/src/test/__snapshots__/index.spec.js.snap +102 -0
- package/src/test/index.spec.js +143 -0
- package/src/tokens.css +8 -0
- package/src/utils/compute-positions-types.d.ts +8 -0
- package/src/utils/compute-positions.ts +65 -0
- package/src/utils/debounce.ts +27 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { useEffect, useState, useRef } from 'react'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
import debounce from 'utils/debounce'
|
|
4
|
+
import { TooltipContent } from 'components/TooltipContent'
|
|
5
|
+
import { useTooltip } from 'components/TooltipProvider'
|
|
6
|
+
import { computeTooltipPosition } from '../../utils/compute-positions'
|
|
7
|
+
import styles from './styles.module.css'
|
|
8
|
+
import type { ITooltip } from './TooltipTypes'
|
|
9
|
+
|
|
10
|
+
const Tooltip = ({
|
|
11
|
+
// props
|
|
12
|
+
id,
|
|
13
|
+
className,
|
|
14
|
+
classNameArrow,
|
|
15
|
+
variant = 'dark',
|
|
16
|
+
anchorId,
|
|
17
|
+
place = 'top',
|
|
18
|
+
offset = 10,
|
|
19
|
+
events = ['hover'],
|
|
20
|
+
positionStrategy = 'absolute',
|
|
21
|
+
wrapper: WrapperElement = 'div',
|
|
22
|
+
children = null,
|
|
23
|
+
delayShow = 0,
|
|
24
|
+
delayHide = 0,
|
|
25
|
+
style: externalStyles,
|
|
26
|
+
// props handled by controller
|
|
27
|
+
isHtmlContent = false,
|
|
28
|
+
content,
|
|
29
|
+
isOpen,
|
|
30
|
+
setIsOpen,
|
|
31
|
+
}: ITooltip) => {
|
|
32
|
+
const tooltipRef = useRef<HTMLElement>(null)
|
|
33
|
+
const tooltipArrowRef = useRef<HTMLDivElement>(null)
|
|
34
|
+
const tooltipShowDelayTimerRef = useRef<NodeJS.Timeout | null>(null)
|
|
35
|
+
const tooltipHideDelayTimerRef = useRef<NodeJS.Timeout | null>(null)
|
|
36
|
+
const [inlineStyles, setInlineStyles] = useState({})
|
|
37
|
+
const [inlineArrowStyles, setInlineArrowStyles] = useState({})
|
|
38
|
+
const [show, setShow] = useState<boolean>(false)
|
|
39
|
+
const [calculatingPosition, setCalculatingPosition] = useState(false)
|
|
40
|
+
const { anchorRefs, setActiveAnchor: setProviderActiveAnchor } = useTooltip()(id)
|
|
41
|
+
const [activeAnchor, setActiveAnchor] = useState<React.RefObject<HTMLElement>>({ current: null })
|
|
42
|
+
|
|
43
|
+
const handleShow = (value: boolean) => {
|
|
44
|
+
if (setIsOpen) {
|
|
45
|
+
setIsOpen(value)
|
|
46
|
+
} else if (isOpen === undefined) {
|
|
47
|
+
setShow(value)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleShowTooltipDelayed = () => {
|
|
52
|
+
if (tooltipShowDelayTimerRef.current) {
|
|
53
|
+
clearTimeout(tooltipShowDelayTimerRef.current)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
tooltipShowDelayTimerRef.current = setTimeout(() => {
|
|
57
|
+
handleShow(true)
|
|
58
|
+
}, delayShow)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handleHideTooltipDelayed = () => {
|
|
62
|
+
if (tooltipHideDelayTimerRef.current) {
|
|
63
|
+
clearTimeout(tooltipHideDelayTimerRef.current)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
tooltipHideDelayTimerRef.current = setTimeout(() => {
|
|
67
|
+
handleShow(false)
|
|
68
|
+
}, delayHide)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const handleShowTooltip = (event?: Event) => {
|
|
72
|
+
if (!event) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
if (delayShow) {
|
|
76
|
+
handleShowTooltipDelayed()
|
|
77
|
+
} else {
|
|
78
|
+
handleShow(true)
|
|
79
|
+
}
|
|
80
|
+
setActiveAnchor((anchor) =>
|
|
81
|
+
anchor.current === event.target ? anchor : { current: event.target as HTMLElement },
|
|
82
|
+
)
|
|
83
|
+
setProviderActiveAnchor({ current: event.target as HTMLElement })
|
|
84
|
+
|
|
85
|
+
if (tooltipHideDelayTimerRef.current) {
|
|
86
|
+
clearTimeout(tooltipHideDelayTimerRef.current)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const handleHideTooltip = () => {
|
|
91
|
+
if (delayHide) {
|
|
92
|
+
handleHideTooltipDelayed()
|
|
93
|
+
} else {
|
|
94
|
+
handleShow(false)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (tooltipShowDelayTimerRef.current) {
|
|
98
|
+
clearTimeout(tooltipShowDelayTimerRef.current)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const handleClickTooltipAnchor = () => {
|
|
103
|
+
if (setIsOpen) {
|
|
104
|
+
setIsOpen(!isOpen)
|
|
105
|
+
} else if (isOpen === undefined) {
|
|
106
|
+
setShow((currentValue) => !currentValue)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// debounce handler to prevent call twice when
|
|
111
|
+
// mouse enter and focus events being triggered toggether
|
|
112
|
+
const debouncedHandleShowTooltip = debounce(handleShowTooltip, 50)
|
|
113
|
+
const debouncedHandleHideTooltip = debounce(handleHideTooltip, 50)
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
const elementRefs = new Set(anchorRefs)
|
|
117
|
+
|
|
118
|
+
const anchorById = document.querySelector(`[id='${anchorId}']`) as HTMLElement
|
|
119
|
+
if (anchorById) {
|
|
120
|
+
setActiveAnchor((anchor) =>
|
|
121
|
+
anchor.current === anchorById ? anchor : { current: anchorById },
|
|
122
|
+
)
|
|
123
|
+
elementRefs.add({ current: anchorById })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!elementRefs.size) {
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
128
|
+
return () => {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const enabledEvents: { event: string; listener: (event?: Event) => void }[] = []
|
|
132
|
+
|
|
133
|
+
if (events.find((event: string) => event === 'click')) {
|
|
134
|
+
enabledEvents.push({ event: 'click', listener: handleClickTooltipAnchor })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (events.find((event: string) => event === 'hover')) {
|
|
138
|
+
enabledEvents.push(
|
|
139
|
+
{ event: 'mouseenter', listener: debouncedHandleShowTooltip },
|
|
140
|
+
{ event: 'mouseleave', listener: debouncedHandleHideTooltip },
|
|
141
|
+
{ event: 'focus', listener: debouncedHandleShowTooltip },
|
|
142
|
+
{ event: 'blur', listener: debouncedHandleHideTooltip },
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
enabledEvents.forEach(({ event, listener }) => {
|
|
147
|
+
elementRefs.forEach((ref) => {
|
|
148
|
+
ref.current?.addEventListener(event, listener)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return () => {
|
|
153
|
+
enabledEvents.forEach(({ event, listener }) => {
|
|
154
|
+
elementRefs.forEach((ref) => {
|
|
155
|
+
ref.current?.removeEventListener(event, listener)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}, [anchorRefs, anchorId, events, delayHide, delayShow])
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
let elementReference = activeAnchor.current
|
|
163
|
+
if (anchorId) {
|
|
164
|
+
// `anchorId` element takes precedence
|
|
165
|
+
elementReference = document.querySelector(`[id='${anchorId}']`) as HTMLElement
|
|
166
|
+
}
|
|
167
|
+
setCalculatingPosition(true)
|
|
168
|
+
let mounted = true
|
|
169
|
+
computeTooltipPosition({
|
|
170
|
+
place,
|
|
171
|
+
offset,
|
|
172
|
+
elementReference,
|
|
173
|
+
tooltipReference: tooltipRef.current,
|
|
174
|
+
tooltipArrowReference: tooltipArrowRef.current,
|
|
175
|
+
strategy: positionStrategy,
|
|
176
|
+
}).then((computedStylesData) => {
|
|
177
|
+
if (!mounted) {
|
|
178
|
+
// invalidate computed positions after remount
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
setCalculatingPosition(false)
|
|
182
|
+
if (Object.keys(computedStylesData.tooltipStyles).length) {
|
|
183
|
+
setInlineStyles(computedStylesData.tooltipStyles)
|
|
184
|
+
}
|
|
185
|
+
if (Object.keys(computedStylesData.tooltipArrowStyles).length) {
|
|
186
|
+
setInlineArrowStyles(computedStylesData.tooltipArrowStyles)
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
return () => {
|
|
190
|
+
mounted = false
|
|
191
|
+
}
|
|
192
|
+
}, [show, isOpen, anchorId, activeAnchor, content, place, offset, positionStrategy])
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
return () => {
|
|
196
|
+
if (tooltipShowDelayTimerRef.current) {
|
|
197
|
+
clearTimeout(tooltipShowDelayTimerRef.current)
|
|
198
|
+
}
|
|
199
|
+
if (tooltipHideDelayTimerRef.current) {
|
|
200
|
+
clearTimeout(tooltipHideDelayTimerRef.current)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}, [])
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<WrapperElement
|
|
207
|
+
id={id}
|
|
208
|
+
role="tooltip"
|
|
209
|
+
className={classNames(styles['tooltip'], styles[variant], className, {
|
|
210
|
+
[styles['show']]: !calculatingPosition && (isOpen || show),
|
|
211
|
+
[styles['fixed']]: positionStrategy === 'fixed',
|
|
212
|
+
})}
|
|
213
|
+
style={{ ...externalStyles, ...inlineStyles }}
|
|
214
|
+
ref={tooltipRef}
|
|
215
|
+
>
|
|
216
|
+
{children || (isHtmlContent ? <TooltipContent content={content as string} /> : content)}
|
|
217
|
+
<div
|
|
218
|
+
className={classNames(styles['arrow'], classNameArrow)}
|
|
219
|
+
style={inlineArrowStyles}
|
|
220
|
+
ref={tooltipArrowRef}
|
|
221
|
+
/>
|
|
222
|
+
</WrapperElement>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export default Tooltip
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ElementType, ReactNode, Element, CSSProperties } from 'react'
|
|
2
|
+
|
|
3
|
+
export type PlacesType = 'top' | 'right' | 'bottom' | 'left'
|
|
4
|
+
|
|
5
|
+
export type VariantType = 'dark' | 'light' | 'success' | 'warning' | 'error' | 'info'
|
|
6
|
+
|
|
7
|
+
export type WrapperType = ElementType | 'div' | 'span'
|
|
8
|
+
|
|
9
|
+
export type ChildrenType = Element | ElementType | ReactNode
|
|
10
|
+
|
|
11
|
+
export type EventsType = 'hover' | 'click'
|
|
12
|
+
|
|
13
|
+
export type PositionStrategy = 'absolute' | 'fixed'
|
|
14
|
+
|
|
15
|
+
export type DataAttribute =
|
|
16
|
+
| 'place'
|
|
17
|
+
| 'content'
|
|
18
|
+
| 'html'
|
|
19
|
+
| 'variant'
|
|
20
|
+
| 'offset'
|
|
21
|
+
| 'wrapper'
|
|
22
|
+
| 'events'
|
|
23
|
+
| 'position-strategy'
|
|
24
|
+
| 'delay-show'
|
|
25
|
+
| 'delay-hide'
|
|
26
|
+
|
|
27
|
+
export interface ITooltip {
|
|
28
|
+
className?: string
|
|
29
|
+
classNameArrow?: string
|
|
30
|
+
content?: string
|
|
31
|
+
html?: string
|
|
32
|
+
place?: PlacesType
|
|
33
|
+
offset?: number
|
|
34
|
+
id?: string
|
|
35
|
+
variant?: VariantType
|
|
36
|
+
anchorId?: string
|
|
37
|
+
isHtmlContent?: boolean
|
|
38
|
+
wrapper?: WrapperType
|
|
39
|
+
children?: ChildrenType
|
|
40
|
+
events?: EventsType[]
|
|
41
|
+
positionStrategy?: PositionStrategy
|
|
42
|
+
delayShow?: number
|
|
43
|
+
delayHide?: number
|
|
44
|
+
style?: CSSProperties
|
|
45
|
+
isOpen?: boolean
|
|
46
|
+
setIsOpen?: (value: boolean) => void
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Tooltip } from './Tooltip'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
.tooltip {
|
|
2
|
+
visibility: hidden;
|
|
3
|
+
width: max-content;
|
|
4
|
+
position: absolute;
|
|
5
|
+
top: 0;
|
|
6
|
+
left: 0;
|
|
7
|
+
padding: 8px 16px;
|
|
8
|
+
border-radius: 3px;
|
|
9
|
+
font-size: 90%;
|
|
10
|
+
pointer-events: none;
|
|
11
|
+
opacity: 0;
|
|
12
|
+
transition: opacity 0.3s ease-out;
|
|
13
|
+
will-change: opacity, visibility;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.fixed {
|
|
17
|
+
position: fixed;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.arrow {
|
|
21
|
+
position: absolute;
|
|
22
|
+
background: inherit;
|
|
23
|
+
width: 8px;
|
|
24
|
+
height: 8px;
|
|
25
|
+
transform: rotate(45deg);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.show {
|
|
29
|
+
visibility: visible;
|
|
30
|
+
opacity: 0.9;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Types variant **/
|
|
34
|
+
.dark {
|
|
35
|
+
background: var(--rt-color-dark);
|
|
36
|
+
color: var(--rt-color-white);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.light {
|
|
40
|
+
background-color: var(--rt-color-white);
|
|
41
|
+
color: var(--rt-color-dark);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.success {
|
|
45
|
+
background-color: var(--rt-color-success);
|
|
46
|
+
color: var(--rt-color-white);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.warning {
|
|
50
|
+
background-color: var(--rt-color-warning);
|
|
51
|
+
color: var(--rt-color-white);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.error {
|
|
55
|
+
background-color: var(--rt-color-error);
|
|
56
|
+
color: var(--rt-color-white);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.info {
|
|
60
|
+
background-color: var(--rt-color-info);
|
|
61
|
+
color: var(--rt-color-white);
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as TooltipContent } from './TooltipContent'
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { Tooltip } from 'components/Tooltip'
|
|
3
|
+
import type {
|
|
4
|
+
EventsType,
|
|
5
|
+
PositionStrategy,
|
|
6
|
+
PlacesType,
|
|
7
|
+
VariantType,
|
|
8
|
+
WrapperType,
|
|
9
|
+
DataAttribute,
|
|
10
|
+
ITooltip,
|
|
11
|
+
} from 'components/Tooltip/TooltipTypes'
|
|
12
|
+
import { useTooltip } from 'components/TooltipProvider'
|
|
13
|
+
import type { ITooltipController } from './TooltipControllerTypes'
|
|
14
|
+
|
|
15
|
+
const TooltipController = ({
|
|
16
|
+
id,
|
|
17
|
+
anchorId,
|
|
18
|
+
content,
|
|
19
|
+
html,
|
|
20
|
+
className,
|
|
21
|
+
classNameArrow,
|
|
22
|
+
variant = 'dark',
|
|
23
|
+
place = 'top',
|
|
24
|
+
offset = 10,
|
|
25
|
+
wrapper = 'div',
|
|
26
|
+
children = null,
|
|
27
|
+
events = ['hover'],
|
|
28
|
+
positionStrategy = 'absolute',
|
|
29
|
+
delayShow = 0,
|
|
30
|
+
delayHide = 0,
|
|
31
|
+
style,
|
|
32
|
+
isOpen,
|
|
33
|
+
setIsOpen,
|
|
34
|
+
}: ITooltipController) => {
|
|
35
|
+
const [tooltipContent, setTooltipContent] = useState(content || html)
|
|
36
|
+
const [tooltipPlace, setTooltipPlace] = useState(place)
|
|
37
|
+
const [tooltipVariant, setTooltipVariant] = useState(variant)
|
|
38
|
+
const [tooltipOffset, setTooltipOffset] = useState(offset)
|
|
39
|
+
const [tooltipDelayShow, setTooltipDelayShow] = useState(delayShow)
|
|
40
|
+
const [tooltipDelayHide, setTooltipDelayHide] = useState(delayHide)
|
|
41
|
+
const [tooltipWrapper, setTooltipWrapper] = useState<WrapperType>(wrapper)
|
|
42
|
+
const [tooltipEvents, setTooltipEvents] = useState(events)
|
|
43
|
+
const [tooltipPositionStrategy, setTooltipPositionStrategy] = useState(positionStrategy)
|
|
44
|
+
const [isHtmlContent, setIsHtmlContent] = useState(Boolean(html))
|
|
45
|
+
const { anchorRefs, activeAnchor } = useTooltip()(id)
|
|
46
|
+
|
|
47
|
+
const getDataAttributesFromAnchorElement = (elementReference: HTMLElement) => {
|
|
48
|
+
const dataAttributes = elementReference?.getAttributeNames().reduce((acc, name) => {
|
|
49
|
+
if (name.startsWith('data-tooltip-')) {
|
|
50
|
+
const parsedAttribute = name.replace(/^data-tooltip-/, '') as DataAttribute
|
|
51
|
+
acc[parsedAttribute] = elementReference?.getAttribute(name) ?? null
|
|
52
|
+
}
|
|
53
|
+
return acc
|
|
54
|
+
}, {} as Record<DataAttribute, string | null>)
|
|
55
|
+
|
|
56
|
+
return dataAttributes
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const applyAllDataAttributesFromAnchorElement = (
|
|
60
|
+
dataAttributes: Record<string, string | null>,
|
|
61
|
+
) => {
|
|
62
|
+
const handleDataAttributes: Record<DataAttribute, (value: string | null) => void> = {
|
|
63
|
+
place: (value) => {
|
|
64
|
+
setTooltipPlace((value as PlacesType) ?? place)
|
|
65
|
+
},
|
|
66
|
+
content: (value) => {
|
|
67
|
+
setIsHtmlContent(false)
|
|
68
|
+
setTooltipContent(value ?? content)
|
|
69
|
+
},
|
|
70
|
+
html: (value) => {
|
|
71
|
+
setIsHtmlContent(!!value)
|
|
72
|
+
setTooltipContent(value ?? html ?? content)
|
|
73
|
+
},
|
|
74
|
+
variant: (value) => {
|
|
75
|
+
setTooltipVariant((value as VariantType) ?? variant)
|
|
76
|
+
},
|
|
77
|
+
offset: (value) => {
|
|
78
|
+
setTooltipOffset(value === null ? offset : Number(value))
|
|
79
|
+
},
|
|
80
|
+
wrapper: (value) => {
|
|
81
|
+
setTooltipWrapper((value as WrapperType) ?? 'div')
|
|
82
|
+
},
|
|
83
|
+
events: (value) => {
|
|
84
|
+
const parsed = value?.split(' ') as EventsType[]
|
|
85
|
+
setTooltipEvents(parsed ?? events)
|
|
86
|
+
},
|
|
87
|
+
'position-strategy': (value) => {
|
|
88
|
+
setTooltipPositionStrategy((value as PositionStrategy) ?? positionStrategy)
|
|
89
|
+
},
|
|
90
|
+
'delay-show': (value) => {
|
|
91
|
+
setTooltipDelayShow(value === null ? delayShow : Number(value))
|
|
92
|
+
},
|
|
93
|
+
'delay-hide': (value) => {
|
|
94
|
+
setTooltipDelayHide(value === null ? delayHide : Number(value))
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
// reset unset data attributes to default values
|
|
98
|
+
// without this, data attributes from the last active anchor will still be used
|
|
99
|
+
Object.values(handleDataAttributes).forEach((handler) => handler(null))
|
|
100
|
+
Object.entries(dataAttributes).forEach(([key, value]) => {
|
|
101
|
+
handleDataAttributes[key as DataAttribute]?.(value)
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (content) {
|
|
107
|
+
setTooltipContent(content)
|
|
108
|
+
}
|
|
109
|
+
if (html) {
|
|
110
|
+
setTooltipContent(html)
|
|
111
|
+
}
|
|
112
|
+
}, [content, html])
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const elementRefs = new Set(anchorRefs)
|
|
116
|
+
|
|
117
|
+
const anchorById = document.querySelector(`[id='${anchorId}']`) as HTMLElement
|
|
118
|
+
if (anchorById) {
|
|
119
|
+
elementRefs.add({ current: anchorById })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!elementRefs.size) {
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
124
|
+
return () => {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const observerCallback: MutationCallback = (mutationList) => {
|
|
128
|
+
mutationList.forEach((mutation) => {
|
|
129
|
+
if (
|
|
130
|
+
!activeAnchor.current ||
|
|
131
|
+
mutation.type !== 'attributes' ||
|
|
132
|
+
!mutation.attributeName?.startsWith('data-tooltip-')
|
|
133
|
+
) {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
// make sure to get all set attributes, since all unset attributes are reset
|
|
137
|
+
const dataAttributes = getDataAttributesFromAnchorElement(activeAnchor.current)
|
|
138
|
+
applyAllDataAttributesFromAnchorElement(dataAttributes)
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Create an observer instance linked to the callback function
|
|
143
|
+
const observer = new MutationObserver(observerCallback)
|
|
144
|
+
|
|
145
|
+
// do not check for subtree and childrens, we only want to know attribute changes
|
|
146
|
+
// to stay watching `data-attributes-*` from anchor element
|
|
147
|
+
const observerConfig = { attributes: true, childList: false, subtree: false }
|
|
148
|
+
|
|
149
|
+
const element = activeAnchor.current ?? anchorById
|
|
150
|
+
|
|
151
|
+
if (element) {
|
|
152
|
+
const dataAttributes = getDataAttributesFromAnchorElement(element)
|
|
153
|
+
applyAllDataAttributesFromAnchorElement(dataAttributes)
|
|
154
|
+
// Start observing the target node for configured mutations
|
|
155
|
+
observer.observe(element, observerConfig)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return () => {
|
|
159
|
+
// Remove the observer when the tooltip is destroyed
|
|
160
|
+
observer.disconnect()
|
|
161
|
+
}
|
|
162
|
+
}, [anchorRefs, activeAnchor, anchorId])
|
|
163
|
+
|
|
164
|
+
const props: ITooltip = {
|
|
165
|
+
id,
|
|
166
|
+
anchorId,
|
|
167
|
+
className,
|
|
168
|
+
classNameArrow,
|
|
169
|
+
content: tooltipContent,
|
|
170
|
+
isHtmlContent,
|
|
171
|
+
place: tooltipPlace,
|
|
172
|
+
variant: tooltipVariant,
|
|
173
|
+
offset: tooltipOffset,
|
|
174
|
+
wrapper: tooltipWrapper,
|
|
175
|
+
events: tooltipEvents,
|
|
176
|
+
positionStrategy: tooltipPositionStrategy,
|
|
177
|
+
delayShow: tooltipDelayShow,
|
|
178
|
+
delayHide: tooltipDelayHide,
|
|
179
|
+
style,
|
|
180
|
+
isOpen,
|
|
181
|
+
setIsOpen,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return children ? <Tooltip {...props}>{children}</Tooltip> : <Tooltip {...props} />
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default TooltipController
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
PlacesType,
|
|
5
|
+
VariantType,
|
|
6
|
+
WrapperType,
|
|
7
|
+
ChildrenType,
|
|
8
|
+
EventsType,
|
|
9
|
+
PositionStrategy,
|
|
10
|
+
} from 'components/Tooltip/TooltipTypes'
|
|
11
|
+
|
|
12
|
+
export interface ITooltipController {
|
|
13
|
+
className?: string
|
|
14
|
+
classNameArrow?: string
|
|
15
|
+
content?: string
|
|
16
|
+
html?: string
|
|
17
|
+
place?: PlacesType
|
|
18
|
+
offset?: number
|
|
19
|
+
id?: string
|
|
20
|
+
variant?: VariantType
|
|
21
|
+
anchorId?: string
|
|
22
|
+
wrapper?: WrapperType
|
|
23
|
+
children?: ChildrenType
|
|
24
|
+
events?: EventsType[]
|
|
25
|
+
positionStrategy?: PositionStrategy
|
|
26
|
+
delayShow?: number
|
|
27
|
+
delayHide?: number
|
|
28
|
+
style?: CSSProperties
|
|
29
|
+
isOpen?: boolean
|
|
30
|
+
setIsOpen?: (value: boolean) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare module 'react' {
|
|
34
|
+
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
|
|
35
|
+
'data-tooltip-place'?: PlacesType
|
|
36
|
+
'data-tooltip-content'?: string
|
|
37
|
+
'data-tooltip-html'?: string
|
|
38
|
+
'data-tooltip-variant'?: VariantType
|
|
39
|
+
'data-tooltip-offset'?: number
|
|
40
|
+
'data-tooltip-wrapper'?: WrapperType
|
|
41
|
+
'data-tooltip-events'?: EventsType[]
|
|
42
|
+
'data-tooltip-position-strategy'?: PositionStrategy
|
|
43
|
+
'data-tooltip-delay-show'?: number
|
|
44
|
+
'data-tooltip-delay-hide'?: number
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as TooltipController } from './TooltipController'
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
PropsWithChildren,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useId,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react'
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
AnchorRef,
|
|
13
|
+
TooltipContextData,
|
|
14
|
+
TooltipContextDataWrapper,
|
|
15
|
+
} from './TooltipProviderTypes'
|
|
16
|
+
|
|
17
|
+
const defaultContextData: TooltipContextData = {
|
|
18
|
+
anchorRefs: new Set(),
|
|
19
|
+
activeAnchor: { current: null },
|
|
20
|
+
attach: () => {
|
|
21
|
+
/* attach anchor element */
|
|
22
|
+
},
|
|
23
|
+
detach: () => {
|
|
24
|
+
/* detach anchor element */
|
|
25
|
+
},
|
|
26
|
+
setActiveAnchor: () => {
|
|
27
|
+
/* set active anchor */
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const defaultContextWrapper = Object.assign(() => defaultContextData, defaultContextData)
|
|
32
|
+
const TooltipContext = createContext<TooltipContextDataWrapper>(defaultContextWrapper)
|
|
33
|
+
|
|
34
|
+
const TooltipProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
|
35
|
+
const defaultTooltipId = useId()
|
|
36
|
+
const [anchorRefMap, setAnchorRefMap] = useState<Record<string, Set<AnchorRef>>>({
|
|
37
|
+
[defaultTooltipId]: new Set(),
|
|
38
|
+
})
|
|
39
|
+
const [activeAnchorMap, setActiveAnchorMap] = useState<Record<string, AnchorRef>>({
|
|
40
|
+
[defaultTooltipId]: { current: null },
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const attach = (tooltipId: string, ...refs: AnchorRef[]) => {
|
|
44
|
+
setAnchorRefMap((oldMap) => {
|
|
45
|
+
const tooltipRefs = oldMap[tooltipId] ?? new Set()
|
|
46
|
+
refs.forEach((ref) => tooltipRefs.add(ref))
|
|
47
|
+
// create new object to trigger re-render
|
|
48
|
+
return { ...oldMap, [tooltipId]: new Set(tooltipRefs) }
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const detach = (tooltipId: string, ...refs: AnchorRef[]) => {
|
|
53
|
+
setAnchorRefMap((oldMap) => {
|
|
54
|
+
const tooltipRefs = oldMap[tooltipId]
|
|
55
|
+
if (!tooltipRefs) {
|
|
56
|
+
// tooltip not found
|
|
57
|
+
// maybe thow error?
|
|
58
|
+
return oldMap
|
|
59
|
+
}
|
|
60
|
+
refs.forEach((ref) => tooltipRefs.delete(ref))
|
|
61
|
+
// create new object to trigger re-render
|
|
62
|
+
return { ...oldMap }
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const setActiveAnchor = (tooltipId: string, ref: React.RefObject<HTMLElement>) => {
|
|
67
|
+
setActiveAnchorMap((oldMap) => {
|
|
68
|
+
if (oldMap[tooltipId]?.current === ref.current) {
|
|
69
|
+
return oldMap
|
|
70
|
+
}
|
|
71
|
+
// create new object to trigger re-render
|
|
72
|
+
return { ...oldMap, [tooltipId]: ref }
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const getTooltipData = 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],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const context = useMemo(() => {
|
|
88
|
+
const contextData: TooltipContextData = getTooltipData(defaultTooltipId)
|
|
89
|
+
const contextWrapper = Object.assign(
|
|
90
|
+
(tooltipId?: string) => getTooltipData(tooltipId),
|
|
91
|
+
contextData,
|
|
92
|
+
)
|
|
93
|
+
return contextWrapper
|
|
94
|
+
}, [getTooltipData])
|
|
95
|
+
|
|
96
|
+
return <TooltipContext.Provider value={context}>{children}</TooltipContext.Provider>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
// this will use the "global" tooltip (same as `useTooltip()()`)
|
|
101
|
+
const { anchorRefs, attach, detach } = useTooltip()
|
|
102
|
+
|
|
103
|
+
// this will use the tooltip with id `tooltip-id`
|
|
104
|
+
const { anchorRefs, attach, detach } = useTooltip()('tooltip-id')
|
|
105
|
+
*/
|
|
106
|
+
export function useTooltip() {
|
|
107
|
+
return useContext(TooltipContext)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default TooltipProvider
|