@pluot/react 0.1.16 → 0.1.17
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/index.js +994 -100
- package/dist-tsc/BrushOverlay.d.ts +31 -0
- package/dist-tsc/BrushOverlay.d.ts.map +1 -0
- package/dist-tsc/BrushOverlay.js +91 -0
- package/dist-tsc/Pluot.d.ts +2 -1
- package/dist-tsc/Pluot.d.ts.map +1 -1
- package/dist-tsc/Pluot.js +59 -22
- package/dist-tsc/Tooltip.d.ts +2 -1
- package/dist-tsc/Tooltip.d.ts.map +1 -1
- package/dist-tsc/Tooltip.js +15 -1
- package/dist-tsc/brush.d.ts +155 -0
- package/dist-tsc/brush.d.ts.map +1 -0
- package/dist-tsc/brush.js +312 -0
- package/dist-tsc/brush.test.d.ts +2 -0
- package/dist-tsc/brush.test.d.ts.map +1 -0
- package/dist-tsc/brush.test.js +487 -0
- package/dist-tsc/index.d.ts +3 -1
- package/dist-tsc/index.d.ts.map +1 -1
- package/dist-tsc/index.js +1 -0
- package/dist-tsc/types.d.ts +255 -0
- package/dist-tsc/types.d.ts.map +1 -0
- package/dist-tsc/types.js +11 -0
- package/dist-tsc/use-brush.d.ts +53 -0
- package/dist-tsc/use-brush.d.ts.map +1 -0
- package/dist-tsc/use-brush.js +360 -0
- package/package.json +5 -3
- package/src/BrushOverlay.tsx +254 -0
- package/src/{Pluot.jsx → Pluot.tsx} +132 -46
- package/src/{Tooltip.jsx → Tooltip.tsx} +19 -3
- package/src/brush.test.ts +590 -0
- package/src/brush.ts +435 -0
- package/src/index.ts +24 -0
- package/src/types.ts +378 -0
- package/src/use-brush.ts +501 -0
- package/src/index.js +0 -2
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer,
|
|
1
|
+
import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer, useId, type CSSProperties } from "react";
|
|
2
2
|
import lzs from "lz-string";
|
|
3
|
-
import {
|
|
3
|
+
import { throttle } from "lodash-es";
|
|
4
4
|
import {
|
|
5
5
|
initialize, getIsWasmReady,
|
|
6
6
|
render_wasm, pick_wasm,
|
|
7
7
|
normalizeStores, getStore,
|
|
8
|
-
getBounds, getCameraMatrixFromBounds,
|
|
9
8
|
checkWebGpuFeatureDetection,
|
|
10
9
|
onMouseMove2d, onWheel2d,
|
|
11
10
|
onMouseMove3d, onWheel3d,
|
|
11
|
+
type CameraMatrix,
|
|
12
12
|
} from '@pluot/core';
|
|
13
13
|
import { Tooltip } from "./Tooltip.js";
|
|
14
|
+
import { BrushOverlay } from "./BrushOverlay.js";
|
|
15
|
+
import { useBrush } from "./use-brush.js";
|
|
16
|
+
import type {
|
|
17
|
+
HoverInfo, PickingResult, PluotProps, RawPickingResult, RenderParams, TooltipContent,
|
|
18
|
+
} from "./types.js";
|
|
14
19
|
|
|
15
20
|
// Needed due to "SyntaxError: Named export 'decompressFromUint8Array' not found.
|
|
16
21
|
// The requested module 'lz-string' is a CommonJS module,
|
|
@@ -32,28 +37,29 @@ const DEFAULT_3D_VIEW = new Float32Array([
|
|
|
32
37
|
0, 0, -10, 1,
|
|
33
38
|
]);
|
|
34
39
|
|
|
35
|
-
const identity = (param) => param;
|
|
40
|
+
const identity = <T,>(param: T): T => param;
|
|
36
41
|
const noop = () => { };
|
|
37
42
|
|
|
38
43
|
// Mouse movement (in pixels) beyond which a mousedown-to-click is
|
|
39
44
|
// considered a drag rather than a click, so that picking is skipped.
|
|
40
45
|
const DRAG_THRESHOLD_PX = 3;
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
// `pick_wasm` is typed `any` by wasm-bindgen, so `RawPickingResult` is what
|
|
48
|
+
// documents its wire format (see types.ts).
|
|
49
|
+
function normalizePickingResult(data: RawPickingResult): PickingResult {
|
|
50
|
+
return {
|
|
51
|
+
...data,
|
|
52
|
+
layer_results: data.layer_results.map(({ layer_id, info }) => ({
|
|
53
|
+
layer_id,
|
|
47
54
|
// This is needed because serde-wasm-bindgen
|
|
48
55
|
// converts Rust HashMap to JS Map.
|
|
49
|
-
info: Object.fromEntries(
|
|
50
|
-
}))
|
|
51
|
-
}
|
|
52
|
-
return result;
|
|
56
|
+
info: Object.fromEntries(info),
|
|
57
|
+
})),
|
|
58
|
+
};
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
|
|
56
|
-
export function Pluot(props) {
|
|
62
|
+
export function Pluot(props: PluotProps) {
|
|
57
63
|
const {
|
|
58
64
|
schemaVersion = null,
|
|
59
65
|
width: widthProp,
|
|
@@ -84,15 +90,35 @@ export function Pluot(props) {
|
|
|
84
90
|
enableTooltip = false,
|
|
85
91
|
onClick: onClickProp = null,
|
|
86
92
|
onHover: onHoverProp = null,
|
|
93
|
+
brushUnitsModeX = "Data",
|
|
94
|
+
brushUnitsModeY = "Data",
|
|
95
|
+
brushMarginTop,
|
|
96
|
+
brushMarginRight,
|
|
97
|
+
brushMarginBottom,
|
|
98
|
+
brushMarginLeft,
|
|
99
|
+
enableBrushCreate = false,
|
|
100
|
+
enableBrushEdit = false,
|
|
101
|
+
enableBrushClear = false,
|
|
102
|
+
brushDelay = 1500,
|
|
103
|
+
maybeBrushDelay = 250,
|
|
104
|
+
persistBrush = false,
|
|
105
|
+
brushMode = "Rect",
|
|
106
|
+
brushColor = "#3b6ea5",
|
|
107
|
+
// An omitted `brush` means uncontrolled; a controlled parent signals the
|
|
108
|
+
// empty state with `NO_BRUSH`, never `undefined`.
|
|
109
|
+
brush = null,
|
|
110
|
+
onBrush,
|
|
111
|
+
onBrushEnd,
|
|
112
|
+
onBrushClear,
|
|
87
113
|
} = props;
|
|
88
114
|
|
|
89
|
-
const onClick = typeof onClickProp === 'function' ? onClickProp :
|
|
90
|
-
const onHover = typeof onHoverProp === 'function' ? onHoverProp : identity;
|
|
115
|
+
const onClick: (result: PickingResult) => void = typeof onClickProp === 'function' ? onClickProp : noop;
|
|
116
|
+
const onHover: (result: PickingResult) => TooltipContent = typeof onHoverProp === 'function' ? onHoverProp : identity;
|
|
91
117
|
|
|
92
118
|
|
|
93
119
|
|
|
94
120
|
// If cameraMatrix is not provided, then we manage the camera matrix internally.
|
|
95
|
-
const [uncontrolledCameraMatrix, setUncontrolledCameraMatrix] = useState(
|
|
121
|
+
const [uncontrolledCameraMatrix, setUncontrolledCameraMatrix] = useState<CameraMatrix>(
|
|
96
122
|
// Note: We use an initializer function here to avoid
|
|
97
123
|
// sharing the same Float32Array among multiple Pluot
|
|
98
124
|
// component instances that may be rendered on the same page.
|
|
@@ -115,7 +141,7 @@ export function Pluot(props) {
|
|
|
115
141
|
const cameraMatrix = isControlledCamera && controlledCameraMatrix !== null
|
|
116
142
|
? controlledCameraMatrix
|
|
117
143
|
: uncontrolledCameraMatrix;
|
|
118
|
-
const setCameraMatrix = isControlledCamera
|
|
144
|
+
const setCameraMatrix: (nextCameraMatrix: CameraMatrix) => void = isControlledCamera
|
|
119
145
|
? setControlledCameraMatrix
|
|
120
146
|
: setUncontrolledCameraMatrix;
|
|
121
147
|
|
|
@@ -136,11 +162,14 @@ export function Pluot(props) {
|
|
|
136
162
|
|
|
137
163
|
const [supportsWebGpu, supportsWebGpuMessage] = useMemo(checkWebGpuFeatureDetection, []);
|
|
138
164
|
|
|
139
|
-
const svgRef = useRef(null);
|
|
140
|
-
const canvasRef = useRef(null);
|
|
141
|
-
const cameraElementRef = useRef(null);
|
|
165
|
+
const svgRef = useRef<SVGSVGElement | null>(null);
|
|
166
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
167
|
+
const cameraElementRef = useRef<HTMLDivElement | null>(null);
|
|
168
|
+
// The outer (width x height) element, which is the coordinate space that both
|
|
169
|
+
// the brush overlay and the hover tooltip are positioned within.
|
|
170
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
142
171
|
|
|
143
|
-
const tempButtonRef = useRef(null);
|
|
172
|
+
const tempButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
144
173
|
|
|
145
174
|
// We may want to update these things without triggering a re-render.
|
|
146
175
|
const isRenderingRef = useRef(false);
|
|
@@ -148,31 +177,53 @@ export function Pluot(props) {
|
|
|
148
177
|
|
|
149
178
|
// Used to distinguish a plain click from a click that ends a drag
|
|
150
179
|
// (e.g. panning), so that dragging does not trigger picking.
|
|
151
|
-
const dragStartRef = useRef(null);
|
|
180
|
+
const dragStartRef = useRef<{ x: number, y: number } | null>(null);
|
|
152
181
|
const didDragRef = useRef(false);
|
|
153
182
|
|
|
154
183
|
// TODO: do we want to use the backlog approach or not?
|
|
155
184
|
// (Similar to the one used in the Vitessce heatmap)
|
|
156
185
|
// Reference: https://github.com/vitessce/vitessce/blob/71f17fb605768e0428fb15ed87b3ea34bcbb4803/packages/view-types/heatmap/src/Heatmap.js#L368
|
|
157
186
|
//const backlogRef = useRef([]);
|
|
158
|
-
const [backlogIteration, incBacklogIteration] = useReducer(i => i + 1, 0);
|
|
187
|
+
const [backlogIteration, incBacklogIteration] = useReducer((i: number) => i + 1, 0);
|
|
159
188
|
|
|
160
189
|
const [isWasmReady, setIsWasmReady] = useState(false);
|
|
161
190
|
const [didFirstRender, setDidFirstRender] = useState(false);
|
|
162
191
|
const [bailedEarly, setBailedEarly] = useState(true);
|
|
163
192
|
|
|
164
|
-
const [pickingResult, setPickingResult] = useState(null);
|
|
165
193
|
// hoverInfo.mouseX/mouseY are in the coordinate space of the outer
|
|
166
194
|
// (width x height) container, used to position the hover tooltip.
|
|
167
|
-
const [hoverInfo, setHoverInfo] = useState(null);
|
|
195
|
+
const [hoverInfo, setHoverInfo] = useState<HoverInfo | null>(null);
|
|
168
196
|
|
|
169
197
|
const progressBarId = useId();
|
|
170
198
|
|
|
199
|
+
const {
|
|
200
|
+
brushState,
|
|
201
|
+
overlayRef: brushOverlayRef,
|
|
202
|
+
geometry: brushGeometry,
|
|
203
|
+
pressProgress,
|
|
204
|
+
isBrushHovered,
|
|
205
|
+
isBrushingRef,
|
|
206
|
+
shouldSuppressClickRef,
|
|
207
|
+
onVertexMouseDown,
|
|
208
|
+
onEdgeMouseDown,
|
|
209
|
+
onClearClick,
|
|
210
|
+
} = useBrush({
|
|
211
|
+
containerRef,
|
|
212
|
+
width, height,
|
|
213
|
+
marginTop, marginRight, marginBottom, marginLeft,
|
|
214
|
+
aspectRatioMode, aspectRatioAlignmentMode, cameraMatrix,
|
|
215
|
+
brushUnitsModeX, brushUnitsModeY,
|
|
216
|
+
brushMarginTop, brushMarginRight, brushMarginBottom, brushMarginLeft,
|
|
217
|
+
enableBrushCreate, enableBrushEdit, enableBrushClear,
|
|
218
|
+
brushDelay, maybeBrushDelay, persistBrush, brushMode,
|
|
219
|
+
brush, onBrush, onBrushEnd, onBrushClear,
|
|
220
|
+
});
|
|
221
|
+
|
|
171
222
|
useLayoutEffect(() => {
|
|
172
223
|
initialize().then(() => setIsWasmReady(getIsWasmReady()));
|
|
173
224
|
}, []);
|
|
174
225
|
|
|
175
|
-
const wheelHandler = useEffectEvent((event) => {
|
|
226
|
+
const wheelHandler = useEffectEvent((event: WheelEvent) => {
|
|
176
227
|
const onWheel = viewMode === "3d" ? onWheel3d : onWheel2d;
|
|
177
228
|
const nextCameraMatrix = onWheel({
|
|
178
229
|
width,
|
|
@@ -189,7 +240,11 @@ export function Pluot(props) {
|
|
|
189
240
|
setCameraMatrix(nextCameraMatrix);
|
|
190
241
|
});
|
|
191
242
|
|
|
192
|
-
const mouseMoveHandler = useEffectEvent((event) => {
|
|
243
|
+
const mouseMoveHandler = useEffectEvent((event: MouseEvent) => {
|
|
244
|
+
// A drag that is drawing or editing a brush must not also pan/rotate the camera.
|
|
245
|
+
if (isBrushingRef.current) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
193
248
|
const onMouseMove = viewMode === "3d" ? onMouseMove3d : onMouseMove2d;
|
|
194
249
|
const nextCameraMatrix = onMouseMove({
|
|
195
250
|
width,
|
|
@@ -208,8 +263,8 @@ export function Pluot(props) {
|
|
|
208
263
|
|
|
209
264
|
// Runs the picking query against the wasm module and returns the normalized result.
|
|
210
265
|
// Shared by the click (pickFrame) and hover (hoverFrame) callbacks below.
|
|
211
|
-
const pick = useEffectEvent(async (screenCoordX, screenCoordY) => {
|
|
212
|
-
const renderParams = {
|
|
266
|
+
const pick = useEffectEvent(async (screenCoordX: number, screenCoordY: number): Promise<PickingResult> => {
|
|
267
|
+
const renderParams: RenderParams = {
|
|
213
268
|
schema_version: schemaVersion,
|
|
214
269
|
width,
|
|
215
270
|
height,
|
|
@@ -251,12 +306,12 @@ export function Pluot(props) {
|
|
|
251
306
|
});
|
|
252
307
|
|
|
253
308
|
// The click-picking callback.
|
|
254
|
-
const pickFrame = useEffectEvent(async (screenCoordX, screenCoordY) => {
|
|
255
|
-
|
|
309
|
+
const pickFrame = useEffectEvent(async (screenCoordX: number, screenCoordY: number) => {
|
|
310
|
+
onClick(await pick(screenCoordX, screenCoordY));
|
|
256
311
|
});
|
|
257
312
|
|
|
258
313
|
// The hover-picking callback.
|
|
259
|
-
const hoverFrame = useEffectEvent(async (screenCoordX, screenCoordY) => {
|
|
314
|
+
const hoverFrame = useEffectEvent(async (screenCoordX: number, screenCoordY: number) => {
|
|
260
315
|
const result = await pick(screenCoordX, screenCoordY);
|
|
261
316
|
setHoverInfo({
|
|
262
317
|
content: onHover(result),
|
|
@@ -292,11 +347,15 @@ export function Pluot(props) {
|
|
|
292
347
|
|
|
293
348
|
// Track mousedown -> mousemove distance so that a drag (e.g. panning)
|
|
294
349
|
// that ends on the camera element does not also trigger a click/pick.
|
|
295
|
-
const mouseDownHandler = (event) => {
|
|
350
|
+
const mouseDownHandler = (event: MouseEvent) => {
|
|
296
351
|
dragStartRef.current = { x: event.clientX, y: event.clientY };
|
|
297
352
|
didDragRef.current = false;
|
|
353
|
+
// A brush drag that ended outside the camera element never produced the
|
|
354
|
+
// click that would have consumed this flag, so clear it as the next
|
|
355
|
+
// interaction begins rather than letting it suppress that one too.
|
|
356
|
+
shouldSuppressClickRef.current = false;
|
|
298
357
|
};
|
|
299
|
-
const dragDetectHandler = (event) => {
|
|
358
|
+
const dragDetectHandler = (event: MouseEvent) => {
|
|
300
359
|
if (!dragStartRef.current) {
|
|
301
360
|
return;
|
|
302
361
|
}
|
|
@@ -310,19 +369,23 @@ export function Pluot(props) {
|
|
|
310
369
|
cameraEl.addEventListener("mousemove", dragDetectHandler);
|
|
311
370
|
|
|
312
371
|
// Set up an onClick handler for picking.
|
|
313
|
-
const clickHandler = (event) => {
|
|
372
|
+
const clickHandler = (event: MouseEvent) => {
|
|
314
373
|
const wasDrag = didDragRef.current;
|
|
374
|
+
// A brush drag (or a click on the clear button) ends with a click on the
|
|
375
|
+
// camera element, which should not also run a picking query.
|
|
376
|
+
const wasBrush = shouldSuppressClickRef.current;
|
|
315
377
|
dragStartRef.current = null;
|
|
316
378
|
didDragRef.current = false;
|
|
317
|
-
|
|
379
|
+
shouldSuppressClickRef.current = false;
|
|
380
|
+
if (enableClick && !wasDrag && !wasBrush) {
|
|
318
381
|
pickFrame(event.offsetX, event.offsetY);
|
|
319
382
|
}
|
|
320
383
|
};
|
|
321
384
|
cameraEl.addEventListener("click", clickHandler);
|
|
322
385
|
|
|
323
386
|
// Set up hover handlers for picking, only when the onHover prop is provided.
|
|
324
|
-
const hoverMoveHandler = (event) => {
|
|
325
|
-
if (enableTooltip) {
|
|
387
|
+
const hoverMoveHandler = (event: MouseEvent) => {
|
|
388
|
+
if (enableTooltip && !isBrushingRef.current) {
|
|
326
389
|
throttledHoverFrame(event.offsetX, event.offsetY);
|
|
327
390
|
}
|
|
328
391
|
};
|
|
@@ -354,7 +417,7 @@ export function Pluot(props) {
|
|
|
354
417
|
isRenderingRef.current = true;
|
|
355
418
|
console.log('wasm.render');
|
|
356
419
|
|
|
357
|
-
const renderParams = {
|
|
420
|
+
const renderParams: RenderParams = {
|
|
358
421
|
schema_version: schemaVersion,
|
|
359
422
|
width,
|
|
360
423
|
height,
|
|
@@ -383,7 +446,7 @@ export function Pluot(props) {
|
|
|
383
446
|
};
|
|
384
447
|
|
|
385
448
|
// Wrap render_wasm in try/catch, to handle Rust panics.
|
|
386
|
-
let arr;
|
|
449
|
+
let arr: Uint8Array;
|
|
387
450
|
try {
|
|
388
451
|
arr = await render_wasm(renderParams);
|
|
389
452
|
|
|
@@ -436,9 +499,9 @@ export function Pluot(props) {
|
|
|
436
499
|
setBailedEarly(false); // Update this to hide the loading indicator.
|
|
437
500
|
|
|
438
501
|
// Clear the LRU cache for the store (via its store_name) corresponding to the rendered plot.
|
|
439
|
-
Object.keys(stores).forEach(storeName => {
|
|
502
|
+
Object.keys(stores ?? {}).forEach(storeName => {
|
|
440
503
|
const storeUsed = getStore(storeName);
|
|
441
|
-
if (storeUsed &&
|
|
504
|
+
if (storeUsed && typeof storeUsed.clearCache === 'function') {
|
|
442
505
|
storeUsed.clearCache();
|
|
443
506
|
}
|
|
444
507
|
});
|
|
@@ -496,7 +559,7 @@ export function Pluot(props) {
|
|
|
496
559
|
|
|
497
560
|
// Position the hover tooltip so that it grows diagonally away from whichever
|
|
498
561
|
// quadrant of the plot the mouse currently occupies, to avoid clipping.
|
|
499
|
-
const hoverStyle = useMemo(() => {
|
|
562
|
+
const hoverStyle = useMemo<CSSProperties | null>(() => {
|
|
500
563
|
if (!hoverInfo) {
|
|
501
564
|
return null;
|
|
502
565
|
}
|
|
@@ -509,6 +572,8 @@ export function Pluot(props) {
|
|
|
509
572
|
return {
|
|
510
573
|
position: "absolute",
|
|
511
574
|
pointerEvents: "none",
|
|
575
|
+
// Above the brush overlay, so a persisted brush does not tint the tooltip.
|
|
576
|
+
zIndex: 2,
|
|
512
577
|
...(isTop ? { top: mouseY + offsetPx } : { bottom: height - mouseY + offsetPx + extraPx }),
|
|
513
578
|
...(isLeft ? { left: mouseX + offsetPx + extraPx } : { right: width - mouseX + offsetPx }),
|
|
514
579
|
};
|
|
@@ -516,7 +581,14 @@ export function Pluot(props) {
|
|
|
516
581
|
|
|
517
582
|
return (
|
|
518
583
|
<>
|
|
519
|
-
<div
|
|
584
|
+
<div
|
|
585
|
+
ref={containerRef}
|
|
586
|
+
style={{
|
|
587
|
+
width, height, position: "relative", backgroundColor,
|
|
588
|
+
// Long-clicking to start a brush otherwise selects surrounding text.
|
|
589
|
+
userSelect: enableBrushCreate ? "none" : undefined,
|
|
590
|
+
}}
|
|
591
|
+
>
|
|
520
592
|
{!supportsWebGpu ? (
|
|
521
593
|
<p>{supportsWebGpuMessage}</p>
|
|
522
594
|
) : null}
|
|
@@ -570,8 +642,22 @@ export function Pluot(props) {
|
|
|
570
642
|
|
|
571
643
|
/>
|
|
572
644
|
)}
|
|
645
|
+
<BrushOverlay
|
|
646
|
+
width={width}
|
|
647
|
+
height={height}
|
|
648
|
+
overlayRef={brushOverlayRef}
|
|
649
|
+
geometry={brushGeometry}
|
|
650
|
+
color={brushColor}
|
|
651
|
+
brushState={brushState}
|
|
652
|
+
pressProgress={pressProgress}
|
|
653
|
+
isBrushHovered={isBrushHovered}
|
|
654
|
+
enableBrushEdit={enableBrushEdit}
|
|
655
|
+
onVertexMouseDown={onVertexMouseDown}
|
|
656
|
+
onEdgeMouseDown={onEdgeMouseDown}
|
|
657
|
+
onClearClick={onClearClick}
|
|
658
|
+
/>
|
|
573
659
|
{hoverInfo ? (
|
|
574
|
-
<div style={hoverStyle}>
|
|
660
|
+
<div style={hoverStyle ?? undefined}>
|
|
575
661
|
<Tooltip content={hoverInfo.content} asTable />
|
|
576
662
|
</div>
|
|
577
663
|
) : null}
|
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import type { TooltipProps } from "./types.js";
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
const boxShadow = '5px 5px 15px rgb(0 0 0 / 20%)';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
// Table cell values come from arbitrary onHover return objects, so narrow
|
|
8
|
+
// them to something React can actually render.
|
|
9
|
+
function renderCell(value: unknown): ReactNode {
|
|
10
|
+
if (value === null || value === undefined || typeof value === "boolean") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
if (React.isValidElement(value)) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Tooltip(props: TooltipProps) {
|
|
7
23
|
const {
|
|
8
24
|
content,
|
|
9
25
|
asTable = false,
|
|
@@ -24,7 +40,7 @@ export function Tooltip(props) {
|
|
|
24
40
|
{Object.entries(content).map(([key, value]) => (
|
|
25
41
|
<tr key={key}>
|
|
26
42
|
<th style={{ border: 'none', fontSize: '12px', outline: 0, padding: '0 2px', textAlign: 'left' }}>{key}</th>
|
|
27
|
-
<td style={{ border: 'none', fontSize: '12px', outline: 0, padding: '0 2px', textAlign: 'left' }}>{value}</td>
|
|
43
|
+
<td style={{ border: 'none', fontSize: '12px', outline: 0, padding: '0 2px', textAlign: 'left' }}>{renderCell(value)}</td>
|
|
28
44
|
</tr>
|
|
29
45
|
))}
|
|
30
46
|
</tbody>
|