react-tooltip 6.0.0-beta.1179.rc.2 → 6.0.0-beta.1179.rc.4

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/README.md CHANGED
@@ -42,6 +42,32 @@ or
42
42
  yarn add react-tooltip
43
43
  ```
44
44
 
45
+ ## React Compatibility
46
+
47
+ `react-tooltip` supports React `16.14.0` and newer through peer dependencies, including React 17, 18, and 19.
48
+
49
+ The project is currently validated against React 19, but the published package remains compatible with older supported React versions.
50
+
51
+ | React version | Supported |
52
+ | ------------- | --------- |
53
+ | 16.14+ | Yes |
54
+ | 17.x | Yes |
55
+ | 18.x | Yes |
56
+ | 19.x | Yes |
57
+
58
+ ## Server Components
59
+
60
+ `react-tooltip` is a client-side library. It uses hooks, DOM observers, browser events, and layout measurement, so the tooltip component itself must run inside a client component boundary.
61
+
62
+ This works well in frameworks such as Next.js with Server Components, but you should render `<Tooltip />` from a client component and attach your tooltip attributes or selectors from elements rendered under that client boundary.
63
+
64
+ If you are using React Server Components, the practical rule is simple:
65
+
66
+ - server components can render the anchor markup
67
+ - client components should render and control `react-tooltip`
68
+
69
+ In Next.js, the usual pattern is to export the tooltip from a small wrapper file marked with `'use client'`.
70
+
45
71
  ## Sponsors
46
72
 
47
73
  ### Gold Sponsors 🌟
@@ -0,0 +1,258 @@
1
+ import { readdir, readFile, writeFile } from 'fs/promises'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import minimist from 'minimist'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const resultsDir = path.join(__dirname, 'results')
8
+
9
+ const args = minimist(process.argv.slice(2))
10
+ const latest = Number(args.latest ?? 3)
11
+ const useAll = Boolean(args.all)
12
+ const generationArg = args.generation
13
+ const useAllGenerations = Boolean(args['all-generations'])
14
+ const explicitFiles = args._.map((inputPath) => path.resolve(inputPath))
15
+
16
+ function aggregateNumbers(values) {
17
+ const sorted = values
18
+ .filter((value) => typeof value === 'number' && Number.isFinite(value))
19
+ .sort((left, right) => left - right)
20
+
21
+ if (sorted.length === 0) {
22
+ return {
23
+ median: null,
24
+ p95: null,
25
+ min: null,
26
+ max: null,
27
+ mean: null,
28
+ standardDeviation: null,
29
+ spreadPercent: null,
30
+ sampleCount: 0,
31
+ }
32
+ }
33
+
34
+ const middle = Math.floor(sorted.length / 2)
35
+ const median =
36
+ sorted.length % 2 === 0
37
+ ? (sorted[middle - 1] + sorted[middle]) / 2
38
+ : sorted[middle]
39
+ const mean = sorted.reduce((total, value) => total + value, 0) / sorted.length
40
+ const variance =
41
+ sorted.reduce((total, value) => total + (value - mean) ** 2, 0) / sorted.length
42
+ const standardDeviation = Math.sqrt(variance)
43
+ const p95 = sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1)]
44
+
45
+ return {
46
+ median,
47
+ p95,
48
+ min: sorted[0],
49
+ max: sorted[sorted.length - 1],
50
+ mean,
51
+ standardDeviation,
52
+ spreadPercent: median === 0 ? null : ((p95 - median) / Math.abs(median)) * 100,
53
+ sampleCount: sorted.length,
54
+ }
55
+ }
56
+
57
+ function formatMs(value) {
58
+ return typeof value === 'number' ? `${value.toFixed(2)} ms` : '—'
59
+ }
60
+
61
+ function formatBytes(value) {
62
+ return typeof value === 'number' ? `${(value / 1024).toFixed(1)} KiB` : '—'
63
+ }
64
+
65
+ function formatPercent(value) {
66
+ return typeof value === 'number' ? `${value.toFixed(1)}%` : '—'
67
+ }
68
+
69
+ function timestampId() {
70
+ return new Date().toISOString().replace(/[:.]/g, '-')
71
+ }
72
+
73
+ function getRunGeneration(run) {
74
+ return Number.isInteger(run?.benchmarkVersion) ? run.benchmarkVersion : 0
75
+ }
76
+
77
+ async function resolveInputFiles() {
78
+ if (explicitFiles.length > 0) {
79
+ return explicitFiles
80
+ }
81
+
82
+ const entries = await readdir(resultsDir)
83
+ const scalingEntries = entries
84
+ .filter((entry) => entry.endsWith('.json'))
85
+ .filter((entry) => entry.startsWith('scaling-'))
86
+ .map((entry) => path.join(resultsDir, entry))
87
+ .sort()
88
+
89
+ if (useAll) {
90
+ return scalingEntries
91
+ }
92
+
93
+ return scalingEntries.slice(-latest)
94
+ }
95
+
96
+ function resolveGenerationSelection(runs) {
97
+ if (useAllGenerations) {
98
+ return {
99
+ selectedRuns: runs,
100
+ generationLabel: 'all benchmark generations',
101
+ }
102
+ }
103
+
104
+ if (generationArg !== undefined) {
105
+ const requestedGeneration =
106
+ generationArg === 'latest' ? Math.max(...runs.map(getRunGeneration)) : Number(generationArg)
107
+
108
+ if (!Number.isInteger(requestedGeneration)) {
109
+ throw new Error(`Invalid --generation value: ${generationArg}`)
110
+ }
111
+
112
+ return {
113
+ selectedRuns: runs.filter((run) => getRunGeneration(run) === requestedGeneration),
114
+ generationLabel:
115
+ generationArg === 'latest'
116
+ ? `latest benchmark generation (${requestedGeneration})`
117
+ : `benchmark generation ${requestedGeneration}`,
118
+ }
119
+ }
120
+
121
+ return {
122
+ selectedRuns: runs,
123
+ generationLabel: 'all benchmark generations',
124
+ }
125
+ }
126
+
127
+ function buildMarkdownReport(result) {
128
+ const lines = [
129
+ '# Aggregated React Tooltip Scaling Benchmark',
130
+ '',
131
+ `- Timestamp: ${result.timestamp}`,
132
+ `- Input files: ${result.inputFiles.length}`,
133
+ `- Selection: ${result.selection}`,
134
+ `- Generation filter: ${result.generationFilter}`,
135
+ `- Counts: ${result.counts.join(', ')}`,
136
+ '',
137
+ '| Count | V5 mount | V6 mount | Mount delta | Mount spread | V5 unmount | V6 unmount | Unmount delta | Unmount spread | V5 mount mem | V6 mount mem | Mount mem delta | Mount mem spread | V5 unmount mem | V6 unmount mem | Unmount mem delta | Unmount mem spread | Samples | V5 timeouts | V6 timeouts |',
138
+ '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |',
139
+ ]
140
+
141
+ result.summary.forEach((row) => {
142
+ lines.push(
143
+ `| ${row.count} | ${formatMs(row.v5.mount.median)} | ${formatMs(row.v6.mount.median)} | ${formatMs(row.mountDeltaMs)} | ${formatPercent(row.mountDeltaSpreadPercent)} | ${formatMs(row.v5.unmount.median)} | ${formatMs(row.v6.unmount.median)} | ${formatMs(row.unmountDeltaMs)} | ${formatPercent(row.unmountDeltaSpreadPercent)} | ${formatBytes(row.v5.mountMemory.median)} | ${formatBytes(row.v6.mountMemory.median)} | ${formatBytes(row.mountMemoryDeltaBytes)} | ${formatPercent(row.mountMemoryDeltaSpreadPercent)} | ${formatBytes(row.v5.unmountMemory.median)} | ${formatBytes(row.v6.unmountMemory.median)} | ${formatBytes(row.unmountMemoryDeltaBytes)} | ${formatPercent(row.unmountMemoryDeltaSpreadPercent)} | ${row.sampleCount} | ${row.v5.timeoutCount} | ${row.v6.timeoutCount} |`,
144
+ )
145
+ })
146
+
147
+ return `${lines.join('\n')}\n`
148
+ }
149
+
150
+ async function main() {
151
+ const inputFiles = await resolveInputFiles()
152
+
153
+ if (inputFiles.length === 0) {
154
+ throw new Error('No benchmark result files were found to aggregate.')
155
+ }
156
+
157
+ const allRuns = await Promise.all(
158
+ inputFiles.map(async (inputFile) => JSON.parse(await readFile(inputFile, 'utf8'))),
159
+ )
160
+ const { selectedRuns: runs, generationLabel } = resolveGenerationSelection(allRuns)
161
+
162
+ if (runs.length === 0) {
163
+ throw new Error('No benchmark result files matched the requested generation filter.')
164
+ }
165
+
166
+ const counts = Array.from(
167
+ new Set(runs.flatMap((run) => run.counts ?? [])),
168
+ ).sort((left, right) => left - right)
169
+
170
+ const summary = counts.map((count) => {
171
+ const rows = runs
172
+ .map((run) => run.summary?.find((entry) => entry.count === count))
173
+ .filter(Boolean)
174
+
175
+ const aggregateVersion = (version) => ({
176
+ mount: aggregateNumbers(rows.map((row) => row[version]?.mount?.median)),
177
+ unmount: aggregateNumbers(rows.map((row) => row[version]?.unmount?.median)),
178
+ mountMemory: aggregateNumbers(rows.map((row) => row[version]?.mountMemory?.median)),
179
+ unmountMemory: aggregateNumbers(rows.map((row) => row[version]?.unmountMemory?.median)),
180
+ timeoutCount: rows.reduce(
181
+ (total, row) => total + (row[version]?.timeoutCount ?? 0),
182
+ 0,
183
+ ),
184
+ })
185
+
186
+ const v5 = aggregateVersion('v5')
187
+ const v6 = aggregateVersion('v6')
188
+ const sampleCount = Math.max(v5.mount.sampleCount, v6.mount.sampleCount)
189
+
190
+ const mountDeltaMs =
191
+ typeof v5.mount.median === 'number' && typeof v6.mount.median === 'number'
192
+ ? v6.mount.median - v5.mount.median
193
+ : null
194
+ const unmountDeltaMs =
195
+ typeof v5.unmount.median === 'number' && typeof v6.unmount.median === 'number'
196
+ ? v6.unmount.median - v5.unmount.median
197
+ : null
198
+ const mountMemoryDeltaBytes =
199
+ typeof v5.mountMemory.median === 'number' && typeof v6.mountMemory.median === 'number'
200
+ ? v6.mountMemory.median - v5.mountMemory.median
201
+ : null
202
+ const unmountMemoryDeltaBytes =
203
+ typeof v5.unmountMemory.median === 'number' && typeof v6.unmountMemory.median === 'number'
204
+ ? v6.unmountMemory.median - v5.unmountMemory.median
205
+ : null
206
+
207
+ return {
208
+ count,
209
+ sampleCount,
210
+ v5,
211
+ v6,
212
+ mountDeltaMs,
213
+ unmountDeltaMs,
214
+ mountMemoryDeltaBytes,
215
+ unmountMemoryDeltaBytes,
216
+ mountDeltaSpreadPercent:
217
+ typeof v5.mount.spreadPercent === 'number' && typeof v6.mount.spreadPercent === 'number'
218
+ ? Math.max(v5.mount.spreadPercent, v6.mount.spreadPercent)
219
+ : null,
220
+ unmountDeltaSpreadPercent:
221
+ typeof v5.unmount.spreadPercent === 'number' && typeof v6.unmount.spreadPercent === 'number'
222
+ ? Math.max(v5.unmount.spreadPercent, v6.unmount.spreadPercent)
223
+ : null,
224
+ mountMemoryDeltaSpreadPercent:
225
+ typeof v5.mountMemory.spreadPercent === 'number' && typeof v6.mountMemory.spreadPercent === 'number'
226
+ ? Math.max(v5.mountMemory.spreadPercent, v6.mountMemory.spreadPercent)
227
+ : null,
228
+ unmountMemoryDeltaSpreadPercent:
229
+ typeof v5.unmountMemory.spreadPercent === 'number' && typeof v6.unmountMemory.spreadPercent === 'number'
230
+ ? Math.max(v5.unmountMemory.spreadPercent, v6.unmountMemory.spreadPercent)
231
+ : null,
232
+ }
233
+ })
234
+
235
+ const result = {
236
+ id: `aggregate-scaling-${timestampId()}`,
237
+ timestamp: new Date().toISOString(),
238
+ selection: useAll ? 'all scaling runs' : `latest ${latest} scaling run(s)`,
239
+ generationFilter: generationLabel,
240
+ inputFiles: runs.map((run, index) => inputFiles[allRuns.indexOf(run)] ?? inputFiles[index]),
241
+ counts,
242
+ summary,
243
+ }
244
+
245
+ const jsonPath = path.join(resultsDir, `${result.id}.json`)
246
+ const markdownPath = path.join(resultsDir, `${result.id}.md`)
247
+
248
+ await writeFile(jsonPath, `${JSON.stringify(result, null, 2)}\n`, 'utf8')
249
+ await writeFile(markdownPath, buildMarkdownReport(result), 'utf8')
250
+
251
+ console.log(`Saved aggregate benchmark results to ${jsonPath}`)
252
+ console.log(`Saved aggregate benchmark report to ${markdownPath}`)
253
+ }
254
+
255
+ main().catch((error) => {
256
+ console.error(error)
257
+ process.exitCode = 1
258
+ })
@@ -0,0 +1,278 @@
1
+ import React, { useEffect, useMemo, useState } from 'react'
2
+ import { flushSync } from 'react-dom'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { Tooltip as TooltipV5 } from '../../docs/node_modules/react-tooltip/dist/react-tooltip.mjs'
5
+ import { Tooltip as TooltipV6 } from '../../dist/react-tooltip.esm.js'
6
+
7
+ type BenchmarkVersion = 'v5' | 'v6'
8
+
9
+ type RenderMode = 'shared'
10
+
11
+ type FixtureState = {
12
+ version: BenchmarkVersion
13
+ count: number
14
+ renderMode: RenderMode
15
+ }
16
+
17
+ type ScenarioSample = {
18
+ count: number
19
+ mountDurationMs: number | null
20
+ unmountDurationMs: number | null
21
+ mountMemoryDeltaBytes: number | null
22
+ unmountMemoryDeltaBytes: number | null
23
+ timedOut: boolean
24
+ }
25
+
26
+ type ScenarioResult = {
27
+ version: BenchmarkVersion
28
+ renderMode: RenderMode
29
+ samplesByCount: ScenarioSample[]
30
+ }
31
+
32
+ type BenchmarkHarness = {
33
+ runScalingBenchmark: (args: {
34
+ version: BenchmarkVersion
35
+ counts: number[]
36
+ timeoutMs: number
37
+ repeats: number
38
+ warmups: number
39
+ renderMode: RenderMode
40
+ onProgress?: (message: string) => void
41
+ }) => Promise<ScenarioResult>
42
+ }
43
+
44
+ declare global {
45
+ interface Window {
46
+ __reactTooltipBenchmark?: BenchmarkHarness
47
+ __REACT_DEVTOOLS_GLOBAL_HOOK__?: {
48
+ isDisabled?: boolean
49
+ }
50
+ gc?: () => void
51
+ }
52
+ }
53
+
54
+ window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
55
+ isDisabled: true,
56
+ }
57
+
58
+ function readUsedHeapBytes() {
59
+ const performanceWithMemory = window.performance as Performance & {
60
+ memory?: {
61
+ usedJSHeapSize: number
62
+ }
63
+ }
64
+
65
+ return performanceWithMemory.memory?.usedJSHeapSize ?? null
66
+ }
67
+
68
+ function nextFrame() {
69
+ return new Promise<void>((resolve) => {
70
+ window.requestAnimationFrame(() => resolve())
71
+ })
72
+ }
73
+
74
+ async function collectGarbage() {
75
+ if (typeof window.gc === 'function') {
76
+ window.gc()
77
+ await nextFrame()
78
+ window.gc()
79
+ await nextFrame()
80
+ }
81
+ }
82
+
83
+ async function readStableHeapBytes() {
84
+ await collectGarbage()
85
+ return readUsedHeapBytes()
86
+ }
87
+
88
+ async function waitUntil(predicate: () => boolean, timeoutMs: number) {
89
+ const startedAt = window.performance.now()
90
+
91
+ while (window.performance.now() - startedAt < timeoutMs) {
92
+ if (predicate()) {
93
+ return true
94
+ }
95
+ await nextFrame()
96
+ }
97
+
98
+ return false
99
+ }
100
+
101
+ function BenchmarkFixture({ version, count }: FixtureState) {
102
+ const TooltipComponent = version === 'v5' ? TooltipV5 : TooltipV6
103
+ const tooltipId = `benchmark-tooltip-${version}`
104
+
105
+ const anchorIds = useMemo(
106
+ () => Array.from({ length: count }, (_, index) => `anchor-${version}-${index}`),
107
+ [count, version],
108
+ )
109
+
110
+ useEffect(() => {
111
+ document.body.setAttribute('data-benchmark-count', String(count))
112
+ document.body.setAttribute('data-benchmark-version', version)
113
+ }, [count, version])
114
+
115
+ return (
116
+ <div className="benchmark-surface" aria-hidden="true">
117
+ <div className="anchor-grid">
118
+ {anchorIds.map((id, index) => (
119
+ <button
120
+ key={id}
121
+ id={id}
122
+ className="anchor"
123
+ data-tooltip-id={tooltipId}
124
+ data-tooltip-content={`Anchor ${index}`}
125
+ type="button"
126
+ >
127
+ {index}
128
+ </button>
129
+ ))}
130
+ </div>
131
+ <TooltipComponent id={tooltipId} />
132
+ </div>
133
+ )
134
+ }
135
+
136
+ const rootNode = document.getElementById('root')
137
+
138
+ if (!rootNode) {
139
+ throw new Error('Benchmark root element was not found.')
140
+ }
141
+
142
+ const benchmarkRoot = createRoot(rootNode)
143
+
144
+ function renderFixture(nextState: FixtureState) {
145
+ return new Promise<void>((resolve) => {
146
+ flushSync(() => {
147
+ benchmarkRoot.render(<BenchmarkFixture {...nextState} />)
148
+ })
149
+ resolve()
150
+ })
151
+ }
152
+
153
+ function unmountFixture() {
154
+ return new Promise<void>((resolve) => {
155
+ flushSync(() => {
156
+ benchmarkRoot.render(<></>)
157
+ })
158
+ resolve()
159
+ })
160
+ }
161
+
162
+ async function runScalingBenchmark({
163
+ version,
164
+ counts,
165
+ timeoutMs,
166
+ repeats,
167
+ warmups,
168
+ renderMode,
169
+ onProgress,
170
+ }: {
171
+ version: BenchmarkVersion
172
+ counts: number[]
173
+ timeoutMs: number
174
+ repeats: number
175
+ warmups: number
176
+ renderMode: RenderMode
177
+ onProgress?: (message: string) => void
178
+ }): Promise<ScenarioResult> {
179
+ const samplesByCount: ScenarioSample[] = []
180
+
181
+ for (const count of counts) {
182
+ for (let warmupIndex = 0; warmupIndex < warmups; warmupIndex += 1) {
183
+ onProgress?.(`count=${count} warmup ${warmupIndex + 1}/${warmups}`)
184
+ await renderFixture({
185
+ version,
186
+ count: 0,
187
+ renderMode,
188
+ })
189
+ await nextFrame()
190
+ await renderFixture({
191
+ version,
192
+ count,
193
+ renderMode,
194
+ })
195
+ await waitUntil(
196
+ () => document.querySelectorAll('[data-tooltip-id]').length === count,
197
+ timeoutMs,
198
+ )
199
+ await nextFrame()
200
+ await unmountFixture()
201
+ await nextFrame()
202
+ }
203
+
204
+ for (let repeatIndex = 0; repeatIndex < repeats; repeatIndex += 1) {
205
+ onProgress?.(`count=${count} repeat ${repeatIndex + 1}/${repeats}`)
206
+ await renderFixture({
207
+ version,
208
+ count: 0,
209
+ renderMode,
210
+ })
211
+ await nextFrame()
212
+
213
+ const mountMemoryBefore = await readStableHeapBytes()
214
+ const mountStartedAt = window.performance.now()
215
+
216
+ await renderFixture({
217
+ version,
218
+ count,
219
+ renderMode,
220
+ })
221
+
222
+ const mountReady = await waitUntil(() => {
223
+ return document.querySelectorAll('[data-tooltip-id]').length === count
224
+ }, timeoutMs)
225
+
226
+ await nextFrame()
227
+
228
+ const mountEndedAt = window.performance.now()
229
+
230
+ const mountMemoryAfter = await readStableHeapBytes()
231
+ const unmountMemoryBefore = mountMemoryAfter
232
+ const unmountStartedAt = window.performance.now()
233
+
234
+ await unmountFixture()
235
+
236
+ const unmountReady = await waitUntil(() => {
237
+ return document.querySelectorAll('[data-tooltip-id]').length === 0
238
+ }, timeoutMs)
239
+
240
+ await nextFrame()
241
+
242
+ const unmountEndedAt = window.performance.now()
243
+
244
+ const unmountMemoryAfter = await readStableHeapBytes()
245
+
246
+ samplesByCount.push({
247
+ count,
248
+ mountDurationMs: mountReady ? mountEndedAt - mountStartedAt : null,
249
+ unmountDurationMs: unmountReady ? unmountEndedAt - unmountStartedAt : null,
250
+ mountMemoryDeltaBytes:
251
+ mountReady && mountMemoryBefore !== null && mountMemoryAfter !== null
252
+ ? mountMemoryAfter - mountMemoryBefore
253
+ : null,
254
+ unmountMemoryDeltaBytes:
255
+ unmountReady && unmountMemoryBefore !== null && unmountMemoryAfter !== null
256
+ ? unmountMemoryAfter - unmountMemoryBefore
257
+ : null,
258
+ timedOut: !mountReady || !unmountReady,
259
+ })
260
+ }
261
+ }
262
+
263
+ return {
264
+ version,
265
+ renderMode,
266
+ samplesByCount,
267
+ }
268
+ }
269
+
270
+ window.__reactTooltipBenchmark = {
271
+ runScalingBenchmark,
272
+ }
273
+
274
+ renderFixture({
275
+ version: 'v6',
276
+ count: 0,
277
+ renderMode: 'shared',
278
+ })
@@ -0,0 +1,54 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>React Tooltip Benchmark</title>
7
+ <style>
8
+ html,
9
+ body {
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ body {
15
+ font-family: sans-serif;
16
+ }
17
+
18
+ #root {
19
+ min-height: 100vh;
20
+ }
21
+
22
+ .benchmark-surface {
23
+ left: 24px;
24
+ opacity: 0.02;
25
+ pointer-events: none;
26
+ position: fixed;
27
+ top: 24px;
28
+ width: 1200px;
29
+ z-index: -1;
30
+ }
31
+
32
+ .anchor-grid {
33
+ display: grid;
34
+ gap: 8px;
35
+ grid-template-columns: repeat(10, minmax(0, 1fr));
36
+ }
37
+
38
+ .anchor {
39
+ align-items: center;
40
+ background: #111827;
41
+ border: 0;
42
+ border-radius: 6px;
43
+ color: #fff;
44
+ display: inline-flex;
45
+ height: 28px;
46
+ justify-content: center;
47
+ }
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <div id="root"></div>
52
+ <script type="module" src="/app.js"></script>
53
+ </body>
54
+ </html>