layerchart 2.0.0-next.53 → 2.0.0-next.54
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/components/Arc.svelte +8 -7
- package/dist/components/Arc.svelte.test.js +1 -1
- package/dist/components/ArcLabel.svelte +1 -1
- package/dist/components/ClipPath.svelte +1 -1
- package/dist/components/GeoClipPath.svelte +75 -0
- package/dist/components/GeoClipPath.svelte.d.ts +29 -0
- package/dist/components/Hull.svelte +20 -2
- package/dist/components/Hull.svelte.d.ts +2 -2
- package/dist/components/Pie.svelte +8 -2
- package/dist/components/Text.svelte +63 -16
- package/dist/components/Text.svelte.d.ts +10 -0
- package/dist/components/charts/BarChart.svelte.test.js +1 -1
- package/dist/components/charts/DefaultTooltip.svelte.test.js +18 -18
- package/dist/components/charts/LineChart.svelte.test.js +1 -1
- package/dist/components/charts/PieChart.svelte.test.js +2 -2
- package/dist/components/charts/__screenshots__/BarChart.svelte.test.ts/BarChart-series-tooltip-should-use-explicit-series-colors--not-color-scale-1.png +0 -0
- package/dist/components/charts/__screenshots__/BarChart.svelte.test.ts/BarChart-series-tooltip-should-use-explicit-series-colors--not-color-scale-2.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-AreaChart--multi-series--quadtree-x-mode--should-fade-non-highlighted-tooltip-series-items-on-hover-1.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-AreaChart--multi-series--quadtree-x-mode--should-fade-non-highlighted-tooltip-series-items-on-hover-2.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-AreaChart--multi-series--quadtree-x-mode--should-show-header-and-all-series-items-1.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-AreaChart--multi-series--quadtree-x-mode--should-show-header-and-all-series-items-2.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-AreaChart--multi-series--quadtree-x-mode--should-show-series-colors-in-tooltip-items-1.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-AreaChart--multi-series--quadtree-x-mode--should-show-series-colors-in-tooltip-items-2.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-AreaChart--multi-series--quadtree-x-mode--should-show-single-series-without-total-1.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-AreaChart--multi-series--quadtree-x-mode--should-show-single-series-without-total-2.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-LineChart--multi-series--quadtree-x-mode--should-show-header-and-all-series-items-1.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-LineChart--multi-series--quadtree-x-mode--should-show-header-and-all-series-items-2.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-ScatterChart--single-point--quadtree-mode--should-show-series-header-for-multi-series-1.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-ScatterChart--single-point--quadtree-mode--should-show-series-header-for-multi-series-2.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-ScatterChart--single-point--quadtree-mode--should-show-x--y--and-r-items-when-r-is-configured-1.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-ScatterChart--single-point--quadtree-mode--should-show-x--y--and-r-items-when-r-is-configured-2.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-ScatterChart--single-point--quadtree-mode--should-show-x-and-y-items-in-tooltip-1.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-ScatterChart--single-point--quadtree-mode--should-show-x-and-y-items-in-tooltip-2.png +0 -0
- package/dist/components/charts/__screenshots__/LineChart.svelte.test.ts/LineChart-tooltip-should-prefer-cScale-color-over-default-series-color-when-cScale-is-explicitly-provided-1.png +0 -0
- package/dist/components/charts/__screenshots__/LineChart.svelte.test.ts/LineChart-tooltip-should-prefer-cScale-color-over-default-series-color-when-cScale-is-explicitly-provided-2.png +0 -0
- package/dist/components/charts/__screenshots__/PieChart.svelte.test.ts/PieChart-uses-hovered-slice-identity-for-implicit-tooltip-series-1.png +0 -0
- package/dist/components/charts/__screenshots__/PieChart.svelte.test.ts/PieChart-uses-hovered-slice-identity-for-implicit-tooltip-series-2.png +0 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/tooltip/Tooltip.svelte +145 -29
- package/dist/components/tooltip/Tooltip.svelte.d.ts +16 -0
- package/dist/components/tooltip/Tooltip.svelte.test.d.ts +1 -0
- package/dist/components/tooltip/Tooltip.svelte.test.js +294 -0
- package/dist/components/tooltip/__screenshots__/Tooltip.svelte.test.ts/Tooltip-portal-should-portal-tooltip-to-a-custom-selector-target-1.png +0 -0
- package/dist/components/tooltip/__screenshots__/Tooltip.svelte.test.ts/Tooltip-portal-should-portal-tooltip-to-a-custom-selector-target-2.png +0 -0
- package/dist/components/tooltip/__screenshots__/Tooltip.svelte.test.ts/Tooltip-portal-should-render-tooltip-inline-when-portal-is-false-1.png +0 -0
- package/dist/components/tooltip/__screenshots__/Tooltip.svelte.test.ts/Tooltip-portal-should-render-tooltip-inline-when-portal-is-false-2.png +0 -0
- package/package.json +13 -13
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import type { PortalOptions } from '@layerstack/svelte-actions';
|
|
3
4
|
import type { Without } from '../../utils/types.js';
|
|
4
5
|
import type { Placement } from '../types.js';
|
|
5
6
|
|
|
@@ -49,6 +50,14 @@
|
|
|
49
50
|
*/
|
|
50
51
|
motion?: MotionProp;
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Duration of the fade in/out transition in milliseconds.
|
|
55
|
+
* Set to `0` to disable the fade transition.
|
|
56
|
+
*
|
|
57
|
+
* @default 100
|
|
58
|
+
*/
|
|
59
|
+
fadeDuration?: number;
|
|
60
|
+
|
|
52
61
|
/**
|
|
53
62
|
* Allow pointer events. Disabled by default to reduce accidental selection, but useful to
|
|
54
63
|
* enable to allow interactive tooltips (using `locked`)
|
|
@@ -132,6 +141,15 @@
|
|
|
132
141
|
content?: HTMLAttributes<HTMLElement>;
|
|
133
142
|
};
|
|
134
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Portal the tooltip outside the chart DOM hierarchy to avoid overflow clipping.
|
|
146
|
+
* Pass `true` to portal to `.PortalTarget` or `document.body`, a string CSS selector,
|
|
147
|
+
* an HTMLElement, or `false` to disable.
|
|
148
|
+
*
|
|
149
|
+
* @default true
|
|
150
|
+
*/
|
|
151
|
+
portal?: PortalOptions;
|
|
152
|
+
|
|
135
153
|
/**
|
|
136
154
|
* Optionally pass the chart's context to the tooltip to get
|
|
137
155
|
* type inference for the data.
|
|
@@ -146,6 +164,7 @@
|
|
|
146
164
|
<script lang="ts" generics="T = any">
|
|
147
165
|
import { fade } from 'svelte/transition';
|
|
148
166
|
import { cls } from '@layerstack/tailwind';
|
|
167
|
+
import { portal as portalAction } from '@layerstack/svelte-actions';
|
|
149
168
|
|
|
150
169
|
import { isScaleBand } from '../../utils/scales.svelte.js';
|
|
151
170
|
import { getChartContext } from '../../contexts/chart.js';
|
|
@@ -157,8 +176,10 @@
|
|
|
157
176
|
anchor = 'top-left',
|
|
158
177
|
classes = {},
|
|
159
178
|
contained = 'container',
|
|
179
|
+
fadeDuration = 100,
|
|
160
180
|
motion = 'spring',
|
|
161
181
|
pointerEvents = false,
|
|
182
|
+
portal: portalProp = true,
|
|
162
183
|
variant = 'default',
|
|
163
184
|
x = 'pointer',
|
|
164
185
|
xOffset = x === 'pointer' ? 10 : 0,
|
|
@@ -189,12 +210,23 @@
|
|
|
189
210
|
return value + (align === 'end' ? -additionalOffset : additionalOffset) - alignOffset;
|
|
190
211
|
}
|
|
191
212
|
|
|
213
|
+
const isPortaled = $derived(
|
|
214
|
+
typeof portalProp === 'boolean' ? portalProp : portalProp?.enabled !== false
|
|
215
|
+
);
|
|
216
|
+
|
|
192
217
|
const positions = $derived.by(() => {
|
|
193
218
|
// if no data or tooltip size is not known yet, return null
|
|
194
219
|
if (!ctx.tooltip.data || tooltipWidth === null || tooltipHeight === null) {
|
|
195
220
|
return { x: null, y: null };
|
|
196
221
|
}
|
|
197
222
|
|
|
223
|
+
// When portaled, we need the container's viewport rect to convert coordinates
|
|
224
|
+
const containerRect = isPortaled ? ctx.containerRef?.getBoundingClientRect() : null;
|
|
225
|
+
// If portaled but container rect not available yet, bail
|
|
226
|
+
if (isPortaled && !containerRect) {
|
|
227
|
+
return { x: null, y: null };
|
|
228
|
+
}
|
|
229
|
+
|
|
198
230
|
const xBandOffset = isScaleBand(ctx.xScale)
|
|
199
231
|
? ctx.xScale.step() / 2 - (ctx.xScale.padding() * ctx.xScale.step()) / 2
|
|
200
232
|
: 0;
|
|
@@ -279,44 +311,75 @@
|
|
|
279
311
|
rect.right = rect.left + tooltipWidth;
|
|
280
312
|
|
|
281
313
|
if (contained === 'container') {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
314
|
+
if (isPortaled && containerRect) {
|
|
315
|
+
// Containment in viewport coordinates
|
|
316
|
+
if (typeof x !== 'number') {
|
|
317
|
+
if (
|
|
318
|
+
(xAlign === 'start' || xAlign === 'center') &&
|
|
319
|
+
containerRect.left + rect.right > containerRect.right
|
|
320
|
+
) {
|
|
321
|
+
rect.left = alignValue(xValue, 'end', xOffset, tooltipWidth);
|
|
322
|
+
}
|
|
323
|
+
if (
|
|
324
|
+
(xAlign === 'end' || xAlign === 'center') &&
|
|
325
|
+
containerRect.left + rect.left < containerRect.left + ctx.padding.left
|
|
326
|
+
) {
|
|
327
|
+
rect.left = alignValue(xValue, 'start', xOffset, tooltipWidth);
|
|
328
|
+
}
|
|
290
329
|
}
|
|
291
|
-
|
|
292
|
-
rect.right = rect.left + tooltipWidth;
|
|
330
|
+
rect.right = rect.left + tooltipWidth;
|
|
293
331
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
332
|
+
if (typeof y !== 'number') {
|
|
333
|
+
if (
|
|
334
|
+
(yAlign === 'start' || yAlign === 'center') &&
|
|
335
|
+
containerRect.top + rect.bottom > containerRect.bottom
|
|
336
|
+
) {
|
|
337
|
+
rect.top = alignValue(yValue, 'end', yOffset, tooltipHeight);
|
|
338
|
+
}
|
|
339
|
+
if (
|
|
340
|
+
(yAlign === 'end' || yAlign === 'center') &&
|
|
341
|
+
containerRect.top + rect.top < containerRect.top + ctx.padding.top
|
|
342
|
+
) {
|
|
343
|
+
rect.top = alignValue(yValue, 'start', yOffset, tooltipHeight);
|
|
344
|
+
}
|
|
297
345
|
}
|
|
298
|
-
|
|
299
|
-
|
|
346
|
+
rect.bottom = rect.top + tooltipHeight;
|
|
347
|
+
} else {
|
|
348
|
+
// Original non-portaled container containment
|
|
349
|
+
if (typeof x !== 'number') {
|
|
350
|
+
// Check if outside of container and swap align side accordingly
|
|
351
|
+
if ((xAlign === 'start' || xAlign === 'center') && rect.right > ctx.containerWidth) {
|
|
352
|
+
rect.left = alignValue(xValue, 'end', xOffset, tooltipWidth);
|
|
353
|
+
}
|
|
354
|
+
if ((xAlign === 'end' || xAlign === 'center') && rect.left < ctx.padding.left) {
|
|
355
|
+
rect.left = alignValue(xValue, 'start', xOffset, tooltipWidth);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
rect.right = rect.left + tooltipWidth;
|
|
359
|
+
|
|
360
|
+
if (typeof y !== 'number') {
|
|
361
|
+
if ((yAlign === 'start' || yAlign === 'center') && rect.bottom > ctx.containerHeight) {
|
|
362
|
+
rect.top = alignValue(yValue, 'end', yOffset, tooltipHeight);
|
|
363
|
+
}
|
|
364
|
+
if ((yAlign === 'end' || yAlign === 'center') && rect.top < ctx.padding.top) {
|
|
365
|
+
rect.top = alignValue(yValue, 'start', yOffset, tooltipHeight);
|
|
366
|
+
}
|
|
300
367
|
}
|
|
368
|
+
rect.bottom = rect.top + tooltipHeight;
|
|
301
369
|
}
|
|
302
|
-
rect.bottom = rect.top + tooltipHeight;
|
|
303
370
|
} else if (contained === 'window') {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (rootRef?.parentElement) {
|
|
307
|
-
const parentViewportRect = rootRef.parentElement.getBoundingClientRect();
|
|
308
|
-
|
|
309
|
-
// Only attempt repositioning if not fixed (ie. `pointer`/`data`)
|
|
371
|
+
if (isPortaled && containerRect) {
|
|
372
|
+
// Already in viewport coordinates, just clamp to window
|
|
310
373
|
if (typeof x !== 'number') {
|
|
311
374
|
if (
|
|
312
375
|
(xAlign === 'start' || xAlign === 'center') &&
|
|
313
|
-
|
|
376
|
+
containerRect.left + rect.right > window.innerWidth
|
|
314
377
|
) {
|
|
315
378
|
rect.left = alignValue(xValue, 'end', xOffset, tooltipWidth);
|
|
316
379
|
}
|
|
317
380
|
if (
|
|
318
381
|
(xAlign === 'end' || xAlign === 'center') &&
|
|
319
|
-
|
|
382
|
+
containerRect.left + rect.left < 0
|
|
320
383
|
) {
|
|
321
384
|
rect.left = alignValue(xValue, 'start', xOffset, tooltipWidth);
|
|
322
385
|
}
|
|
@@ -326,20 +389,67 @@
|
|
|
326
389
|
if (typeof y !== 'number') {
|
|
327
390
|
if (
|
|
328
391
|
(yAlign === 'start' || yAlign === 'center') &&
|
|
329
|
-
|
|
392
|
+
containerRect.top + rect.bottom > window.innerHeight
|
|
330
393
|
) {
|
|
331
394
|
rect.top = alignValue(yValue, 'end', yOffset, tooltipHeight);
|
|
332
395
|
}
|
|
333
|
-
if (
|
|
396
|
+
if (
|
|
397
|
+
(yAlign === 'end' || yAlign === 'center') &&
|
|
398
|
+
containerRect.top + rect.top < 0
|
|
399
|
+
) {
|
|
334
400
|
rect.top = alignValue(yValue, 'start', yOffset, tooltipHeight);
|
|
335
401
|
}
|
|
336
402
|
}
|
|
337
403
|
rect.bottom = rect.top + tooltipHeight;
|
|
404
|
+
} else {
|
|
405
|
+
// Original non-portaled window containment
|
|
406
|
+
// Root <div> won't be available on initial mount
|
|
407
|
+
if (rootRef?.parentElement) {
|
|
408
|
+
const parentViewportRect = rootRef.parentElement.getBoundingClientRect();
|
|
409
|
+
|
|
410
|
+
// Only attempt repositioning if not fixed (ie. `pointer`/`data`)
|
|
411
|
+
if (typeof x !== 'number') {
|
|
412
|
+
if (
|
|
413
|
+
(xAlign === 'start' || xAlign === 'center') &&
|
|
414
|
+
parentViewportRect.left + rect.right > window.innerWidth
|
|
415
|
+
) {
|
|
416
|
+
rect.left = alignValue(xValue, 'end', xOffset, tooltipWidth);
|
|
417
|
+
}
|
|
418
|
+
if (
|
|
419
|
+
(xAlign === 'end' || xAlign === 'center') &&
|
|
420
|
+
parentViewportRect.left + rect.left < 0
|
|
421
|
+
) {
|
|
422
|
+
rect.left = alignValue(xValue, 'start', xOffset, tooltipWidth);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
rect.right = rect.left + tooltipWidth;
|
|
426
|
+
|
|
427
|
+
if (typeof y !== 'number') {
|
|
428
|
+
if (
|
|
429
|
+
(yAlign === 'start' || yAlign === 'center') &&
|
|
430
|
+
parentViewportRect.top + rect.bottom > window.innerHeight
|
|
431
|
+
) {
|
|
432
|
+
rect.top = alignValue(yValue, 'end', yOffset, tooltipHeight);
|
|
433
|
+
}
|
|
434
|
+
if (
|
|
435
|
+
(yAlign === 'end' || yAlign === 'center') &&
|
|
436
|
+
parentViewportRect.top + rect.top < 0
|
|
437
|
+
) {
|
|
438
|
+
rect.top = alignValue(yValue, 'start', yOffset, tooltipHeight);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
rect.bottom = rect.top + tooltipHeight;
|
|
442
|
+
}
|
|
338
443
|
}
|
|
339
444
|
}
|
|
445
|
+
|
|
446
|
+
// When portaled, convert from container-relative to viewport-relative coordinates
|
|
447
|
+
const offsetX = isPortaled && containerRect ? containerRect.left : 0;
|
|
448
|
+
const offsetY = isPortaled && containerRect ? containerRect.top : 0;
|
|
449
|
+
|
|
340
450
|
return {
|
|
341
|
-
x: rect.left,
|
|
342
|
-
y: rect.top,
|
|
451
|
+
x: rect.left + offsetX,
|
|
452
|
+
y: rect.top + offsetY,
|
|
343
453
|
};
|
|
344
454
|
});
|
|
345
455
|
|
|
@@ -356,11 +466,13 @@
|
|
|
356
466
|
{#if ctx.tooltip.data}
|
|
357
467
|
<div
|
|
358
468
|
{...props.root}
|
|
469
|
+
use:portalAction={portalProp}
|
|
359
470
|
class={cls('lc-tooltip-root', classes.root, props.root?.class)}
|
|
360
471
|
class:disablePointerEvents={pointerEvents === false}
|
|
472
|
+
class:portaled={isPortaled}
|
|
361
473
|
style:top="{motionY.current}px"
|
|
362
474
|
style:left="{motionX.current}px"
|
|
363
|
-
transition:fade={{ duration:
|
|
475
|
+
transition:fade={{ duration: fadeDuration }}
|
|
364
476
|
bind:clientWidth={tooltipWidth}
|
|
365
477
|
bind:clientHeight={tooltipHeight}
|
|
366
478
|
bind:this={rootRef}
|
|
@@ -392,6 +504,10 @@
|
|
|
392
504
|
z-index: 50;
|
|
393
505
|
user-select: none;
|
|
394
506
|
|
|
507
|
+
&.portaled {
|
|
508
|
+
position: fixed;
|
|
509
|
+
}
|
|
510
|
+
|
|
395
511
|
&.disablePointerEvents {
|
|
396
512
|
pointer-events: none;
|
|
397
513
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import type { PortalOptions } from '@layerstack/svelte-actions';
|
|
2
3
|
import type { Without } from '../../utils/types.js';
|
|
3
4
|
import type { Placement } from '../types.js';
|
|
4
5
|
export type Align = 'start' | 'center' | 'end';
|
|
@@ -41,6 +42,13 @@ export type TooltipPropsWithoutHTML<T = any> = {
|
|
|
41
42
|
* @default "spring"
|
|
42
43
|
*/
|
|
43
44
|
motion?: MotionProp;
|
|
45
|
+
/**
|
|
46
|
+
* Duration of the fade in/out transition in milliseconds.
|
|
47
|
+
* Set to `0` to disable the fade transition.
|
|
48
|
+
*
|
|
49
|
+
* @default 100
|
|
50
|
+
*/
|
|
51
|
+
fadeDuration?: number;
|
|
44
52
|
/**
|
|
45
53
|
* Allow pointer events. Disabled by default to reduce accidental selection, but useful to
|
|
46
54
|
* enable to allow interactive tooltips (using `locked`)
|
|
@@ -115,6 +123,14 @@ export type TooltipPropsWithoutHTML<T = any> = {
|
|
|
115
123
|
*/
|
|
116
124
|
content?: HTMLAttributes<HTMLElement>;
|
|
117
125
|
};
|
|
126
|
+
/**
|
|
127
|
+
* Portal the tooltip outside the chart DOM hierarchy to avoid overflow clipping.
|
|
128
|
+
* Pass `true` to portal to `.PortalTarget` or `document.body`, a string CSS selector,
|
|
129
|
+
* an HTMLElement, or `false` to disable.
|
|
130
|
+
*
|
|
131
|
+
* @default true
|
|
132
|
+
*/
|
|
133
|
+
portal?: PortalOptions;
|
|
118
134
|
/**
|
|
119
135
|
* Optionally pass the chart's context to the tooltip to get
|
|
120
136
|
* type inference for the data.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { render } from 'vitest-browser-svelte';
|
|
3
|
+
import LineChart from '../charts/LineChart.svelte';
|
|
4
|
+
const data = [
|
|
5
|
+
{ date: 0, value: 10 },
|
|
6
|
+
{ date: 1, value: 30 },
|
|
7
|
+
{ date: 2, value: 20 },
|
|
8
|
+
{ date: 3, value: 50 },
|
|
9
|
+
{ date: 4, value: 40 },
|
|
10
|
+
];
|
|
11
|
+
const baseProps = {
|
|
12
|
+
data,
|
|
13
|
+
x: 'date',
|
|
14
|
+
y: 'value',
|
|
15
|
+
height: 300,
|
|
16
|
+
width: 400,
|
|
17
|
+
tooltipContext: { mode: 'bisect-x' },
|
|
18
|
+
};
|
|
19
|
+
/** Dispatch pointer events to trigger the tooltip on a given element */
|
|
20
|
+
function triggerTooltip(el, position) {
|
|
21
|
+
const rect = el.getBoundingClientRect();
|
|
22
|
+
const eventInit = {
|
|
23
|
+
bubbles: true,
|
|
24
|
+
clientX: position?.clientX ?? rect.x + rect.width / 2,
|
|
25
|
+
clientY: position?.clientY ?? rect.y + rect.height / 2,
|
|
26
|
+
};
|
|
27
|
+
el.dispatchEvent(new PointerEvent('pointerenter', eventInit));
|
|
28
|
+
el.dispatchEvent(new PointerEvent('pointermove', eventInit));
|
|
29
|
+
}
|
|
30
|
+
describe('Tooltip', () => {
|
|
31
|
+
describe('portal', () => {
|
|
32
|
+
it('should portal tooltip to body by default', async () => {
|
|
33
|
+
const { container } = render(LineChart, {
|
|
34
|
+
props: baseProps,
|
|
35
|
+
});
|
|
36
|
+
const tooltipCtx = container.querySelector('.lc-tooltip-context');
|
|
37
|
+
await expect.element(tooltipCtx).toBeInTheDocument();
|
|
38
|
+
triggerTooltip(tooltipCtx);
|
|
39
|
+
await vi.waitFor(() => {
|
|
40
|
+
// Tooltip root should be portaled to body (outside the chart container)
|
|
41
|
+
const tooltipInBody = document.body.querySelector('.lc-tooltip-root');
|
|
42
|
+
expect(tooltipInBody).not.toBeNull();
|
|
43
|
+
// Should use fixed positioning when portaled
|
|
44
|
+
const style = getComputedStyle(tooltipInBody);
|
|
45
|
+
expect(style.position).toBe('fixed');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('should render tooltip inline when portal is false', async () => {
|
|
49
|
+
const { container } = render(LineChart, {
|
|
50
|
+
props: {
|
|
51
|
+
...baseProps,
|
|
52
|
+
props: { tooltip: { root: { portal: false } } },
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const tooltipCtx = container.querySelector('.lc-tooltip-context');
|
|
56
|
+
await expect.element(tooltipCtx).toBeInTheDocument();
|
|
57
|
+
triggerTooltip(tooltipCtx);
|
|
58
|
+
await vi.waitFor(() => {
|
|
59
|
+
// Tooltip root should be inside the chart container
|
|
60
|
+
const tooltipInContainer = container.querySelector('.lc-tooltip-root');
|
|
61
|
+
expect(tooltipInContainer).not.toBeNull();
|
|
62
|
+
// Should use absolute positioning when not portaled
|
|
63
|
+
const style = getComputedStyle(tooltipInContainer);
|
|
64
|
+
expect(style.position).toBe('absolute');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
it('should portal tooltip to a custom selector target', async () => {
|
|
68
|
+
// Create a custom portal target
|
|
69
|
+
const portalTarget = document.createElement('div');
|
|
70
|
+
portalTarget.className = 'custom-portal-target';
|
|
71
|
+
document.body.appendChild(portalTarget);
|
|
72
|
+
try {
|
|
73
|
+
const { container } = render(LineChart, {
|
|
74
|
+
props: {
|
|
75
|
+
...baseProps,
|
|
76
|
+
props: { tooltip: { root: { portal: { target: '.custom-portal-target' } } } },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
const tooltipCtx = container.querySelector('.lc-tooltip-context');
|
|
80
|
+
await expect.element(tooltipCtx).toBeInTheDocument();
|
|
81
|
+
triggerTooltip(tooltipCtx);
|
|
82
|
+
await vi.waitFor(() => {
|
|
83
|
+
const tooltipInTarget = portalTarget.querySelector('.lc-tooltip-root');
|
|
84
|
+
expect(tooltipInTarget).not.toBeNull();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
portalTarget.remove();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
it('should show tooltip content when portaled', async () => {
|
|
92
|
+
const { container } = render(LineChart, {
|
|
93
|
+
props: baseProps,
|
|
94
|
+
});
|
|
95
|
+
const tooltipCtx = container.querySelector('.lc-tooltip-context');
|
|
96
|
+
await expect.element(tooltipCtx).toBeInTheDocument();
|
|
97
|
+
triggerTooltip(tooltipCtx);
|
|
98
|
+
await vi.waitFor(() => {
|
|
99
|
+
const tooltipRoot = document.body.querySelector('.lc-tooltip-root');
|
|
100
|
+
expect(tooltipRoot).not.toBeNull();
|
|
101
|
+
// Should contain tooltip content (items from default tooltip)
|
|
102
|
+
const tooltipItems = tooltipRoot.querySelectorAll('.lc-tooltip-item-root');
|
|
103
|
+
expect(tooltipItems.length).toBeGreaterThan(0);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
it('should have numeric top and left styles when portaled', async () => {
|
|
107
|
+
const { container } = render(LineChart, {
|
|
108
|
+
props: baseProps,
|
|
109
|
+
});
|
|
110
|
+
const tooltipCtx = container.querySelector('.lc-tooltip-context');
|
|
111
|
+
await expect.element(tooltipCtx).toBeInTheDocument();
|
|
112
|
+
triggerTooltip(tooltipCtx);
|
|
113
|
+
await vi.waitFor(() => {
|
|
114
|
+
const tooltipRoot = document.body.querySelector('.lc-tooltip-root');
|
|
115
|
+
expect(tooltipRoot).not.toBeNull();
|
|
116
|
+
// Should have valid pixel positions (not NaN or empty)
|
|
117
|
+
const top = tooltipRoot.style.top;
|
|
118
|
+
const left = tooltipRoot.style.left;
|
|
119
|
+
expect(top).toMatch(/^-?\d+(\.\d+)?px$/);
|
|
120
|
+
expect(left).toMatch(/^-?\d+(\.\d+)?px$/);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe('contained="container" (default)', () => {
|
|
125
|
+
it('should flip tooltip left when pointer is near the right edge', async () => {
|
|
126
|
+
const { container } = render(LineChart, {
|
|
127
|
+
props: baseProps,
|
|
128
|
+
});
|
|
129
|
+
const tooltipCtx = container.querySelector('.lc-tooltip-context');
|
|
130
|
+
await expect.element(tooltipCtx).toBeInTheDocument();
|
|
131
|
+
const ctxRect = tooltipCtx.getBoundingClientRect();
|
|
132
|
+
// Trigger near the right edge of the container
|
|
133
|
+
triggerTooltip(tooltipCtx, {
|
|
134
|
+
clientX: ctxRect.right - 5,
|
|
135
|
+
clientY: ctxRect.top + ctxRect.height / 2,
|
|
136
|
+
});
|
|
137
|
+
await vi.waitFor(() => {
|
|
138
|
+
const tooltipRoot = document.body.querySelector('.lc-tooltip-root');
|
|
139
|
+
expect(tooltipRoot).not.toBeNull();
|
|
140
|
+
const tooltipLeft = parseFloat(tooltipRoot.style.left);
|
|
141
|
+
// Tooltip should be positioned to the LEFT of the pointer (flipped),
|
|
142
|
+
// so its left edge should be less than the pointer position
|
|
143
|
+
expect(tooltipLeft).toBeLessThan(ctxRect.right - 5);
|
|
144
|
+
// And specifically, the tooltip's right edge should not exceed the container
|
|
145
|
+
expect(tooltipLeft + tooltipRoot.offsetWidth).toBeLessThanOrEqual(ctxRect.right + 1);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
it('should flip tooltip right when pointer is near the left edge', async () => {
|
|
149
|
+
const { container } = render(LineChart, {
|
|
150
|
+
props: {
|
|
151
|
+
...baseProps,
|
|
152
|
+
props: { tooltip: { root: { anchor: 'top-right' } } },
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const tooltipCtx = container.querySelector('.lc-tooltip-context');
|
|
156
|
+
await expect.element(tooltipCtx).toBeInTheDocument();
|
|
157
|
+
const ctxRect = tooltipCtx.getBoundingClientRect();
|
|
158
|
+
// Trigger near the left edge of the container
|
|
159
|
+
triggerTooltip(tooltipCtx, {
|
|
160
|
+
clientX: ctxRect.left + 5,
|
|
161
|
+
clientY: ctxRect.top + ctxRect.height / 2,
|
|
162
|
+
});
|
|
163
|
+
await vi.waitFor(() => {
|
|
164
|
+
const tooltipRoot = document.body.querySelector('.lc-tooltip-root');
|
|
165
|
+
expect(tooltipRoot).not.toBeNull();
|
|
166
|
+
const tooltipLeft = parseFloat(tooltipRoot.style.left);
|
|
167
|
+
// Tooltip should be positioned to the RIGHT of the pointer (flipped),
|
|
168
|
+
// so its left edge should be >= the container's left edge
|
|
169
|
+
expect(tooltipLeft).toBeGreaterThanOrEqual(ctxRect.left - 1);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
it('should flip tooltip up when pointer is near the bottom edge', async () => {
|
|
173
|
+
const { container } = render(LineChart, {
|
|
174
|
+
props: baseProps,
|
|
175
|
+
});
|
|
176
|
+
const tooltipCtx = container.querySelector('.lc-tooltip-context');
|
|
177
|
+
await expect.element(tooltipCtx).toBeInTheDocument();
|
|
178
|
+
const ctxRect = tooltipCtx.getBoundingClientRect();
|
|
179
|
+
// Trigger near the bottom edge of the container
|
|
180
|
+
triggerTooltip(tooltipCtx, {
|
|
181
|
+
clientX: ctxRect.left + ctxRect.width / 2,
|
|
182
|
+
clientY: ctxRect.bottom - 5,
|
|
183
|
+
});
|
|
184
|
+
await vi.waitFor(() => {
|
|
185
|
+
const tooltipRoot = document.body.querySelector('.lc-tooltip-root');
|
|
186
|
+
expect(tooltipRoot).not.toBeNull();
|
|
187
|
+
const tooltipTop = parseFloat(tooltipRoot.style.top);
|
|
188
|
+
// Tooltip should be positioned ABOVE the pointer (flipped),
|
|
189
|
+
// so its bottom edge should not exceed the container
|
|
190
|
+
expect(tooltipTop + tooltipRoot.offsetHeight).toBeLessThanOrEqual(ctxRect.bottom + 1);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
it('should flip tooltip down when pointer is near the top edge', async () => {
|
|
194
|
+
const { container } = render(LineChart, {
|
|
195
|
+
props: {
|
|
196
|
+
...baseProps,
|
|
197
|
+
props: { tooltip: { root: { anchor: 'bottom-left' } } },
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
const tooltipCtx = container.querySelector('.lc-tooltip-context');
|
|
201
|
+
await expect.element(tooltipCtx).toBeInTheDocument();
|
|
202
|
+
const ctxRect = tooltipCtx.getBoundingClientRect();
|
|
203
|
+
// Trigger near the top edge of the container
|
|
204
|
+
triggerTooltip(tooltipCtx, {
|
|
205
|
+
clientX: ctxRect.left + ctxRect.width / 2,
|
|
206
|
+
clientY: ctxRect.top + 5,
|
|
207
|
+
});
|
|
208
|
+
await vi.waitFor(() => {
|
|
209
|
+
const tooltipRoot = document.body.querySelector('.lc-tooltip-root');
|
|
210
|
+
expect(tooltipRoot).not.toBeNull();
|
|
211
|
+
const tooltipTop = parseFloat(tooltipRoot.style.top);
|
|
212
|
+
// Tooltip should be positioned BELOW the pointer (flipped),
|
|
213
|
+
// so its top edge should be >= the container's top
|
|
214
|
+
expect(tooltipTop).toBeGreaterThanOrEqual(ctxRect.top - 1);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('contained="window"', () => {
|
|
219
|
+
it('should flip tooltip left when it would overflow the right side of the viewport', async () => {
|
|
220
|
+
const { container } = render(LineChart, {
|
|
221
|
+
props: {
|
|
222
|
+
...baseProps,
|
|
223
|
+
props: { tooltip: { root: { contained: 'window' } } },
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
const tooltipCtx = container.querySelector('.lc-tooltip-context');
|
|
227
|
+
await expect.element(tooltipCtx).toBeInTheDocument();
|
|
228
|
+
const ctxRect = tooltipCtx.getBoundingClientRect();
|
|
229
|
+
// Trigger near the right edge of the container
|
|
230
|
+
triggerTooltip(tooltipCtx, {
|
|
231
|
+
clientX: ctxRect.right - 5,
|
|
232
|
+
clientY: ctxRect.top + ctxRect.height / 2,
|
|
233
|
+
});
|
|
234
|
+
await vi.waitFor(() => {
|
|
235
|
+
const tooltipRoot = document.body.querySelector('.lc-tooltip-root');
|
|
236
|
+
expect(tooltipRoot).not.toBeNull();
|
|
237
|
+
const tooltipLeft = parseFloat(tooltipRoot.style.left);
|
|
238
|
+
// Tooltip should not overflow the right side of the viewport
|
|
239
|
+
expect(tooltipLeft + tooltipRoot.offsetWidth).toBeLessThanOrEqual(window.innerWidth + 1);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
it('should flip tooltip up when it would overflow the bottom of the viewport', async () => {
|
|
243
|
+
const { container } = render(LineChart, {
|
|
244
|
+
props: {
|
|
245
|
+
...baseProps,
|
|
246
|
+
props: { tooltip: { root: { contained: 'window' } } },
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
const tooltipCtx = container.querySelector('.lc-tooltip-context');
|
|
250
|
+
await expect.element(tooltipCtx).toBeInTheDocument();
|
|
251
|
+
const ctxRect = tooltipCtx.getBoundingClientRect();
|
|
252
|
+
// Trigger near the bottom edge of the container
|
|
253
|
+
triggerTooltip(tooltipCtx, {
|
|
254
|
+
clientX: ctxRect.left + ctxRect.width / 2,
|
|
255
|
+
clientY: ctxRect.bottom - 5,
|
|
256
|
+
});
|
|
257
|
+
await vi.waitFor(() => {
|
|
258
|
+
const tooltipRoot = document.body.querySelector('.lc-tooltip-root');
|
|
259
|
+
expect(tooltipRoot).not.toBeNull();
|
|
260
|
+
const tooltipTop = parseFloat(tooltipRoot.style.top);
|
|
261
|
+
// Tooltip should not overflow the bottom of the viewport
|
|
262
|
+
expect(tooltipTop + tooltipRoot.offsetHeight).toBeLessThanOrEqual(window.innerHeight + 1);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
describe('contained={false}', () => {
|
|
267
|
+
it('should not constrain tooltip position', async () => {
|
|
268
|
+
const { container } = render(LineChart, {
|
|
269
|
+
props: {
|
|
270
|
+
...baseProps,
|
|
271
|
+
props: { tooltip: { root: { contained: false } } },
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
const tooltipCtx = container.querySelector('.lc-tooltip-context');
|
|
275
|
+
await expect.element(tooltipCtx).toBeInTheDocument();
|
|
276
|
+
const ctxRect = tooltipCtx.getBoundingClientRect();
|
|
277
|
+
// Trigger near the right edge — tooltip should NOT flip
|
|
278
|
+
triggerTooltip(tooltipCtx, {
|
|
279
|
+
clientX: ctxRect.right - 5,
|
|
280
|
+
clientY: ctxRect.top + ctxRect.height / 2,
|
|
281
|
+
});
|
|
282
|
+
await vi.waitFor(() => {
|
|
283
|
+
const tooltipRoot = document.body.querySelector('.lc-tooltip-root');
|
|
284
|
+
expect(tooltipRoot).not.toBeNull();
|
|
285
|
+
const tooltipLeft = parseFloat(tooltipRoot.style.left);
|
|
286
|
+
// With default anchor='top-left', tooltip is placed to the right of pointer.
|
|
287
|
+
// Since contained=false, the tooltip left should be near/past the pointer x
|
|
288
|
+
// (i.e., it doesn't flip like contained="container" would)
|
|
289
|
+
const pointerViewportX = ctxRect.right - 5;
|
|
290
|
+
expect(tooltipLeft).toBeGreaterThanOrEqual(pointerViewportX - 1);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "techniq/layerchart",
|
|
7
7
|
"homepage": "https://layerchart.com",
|
|
8
|
-
"version": "2.0.0-next.
|
|
8
|
+
"version": "2.0.0-next.54",
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"@changesets/cli": "^2.30.0",
|
|
11
11
|
"@napi-rs/canvas": "^0.1.97",
|
|
12
12
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
13
|
-
"@sveltejs/kit": "^2.
|
|
13
|
+
"@sveltejs/kit": "^2.57.1",
|
|
14
14
|
"@sveltejs/package": "^2.5.7",
|
|
15
15
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
16
16
|
"@svitejs/changesets-changelog-github-compact": "^1.2.0",
|
|
@@ -33,19 +33,19 @@
|
|
|
33
33
|
"@types/d3-scale-chromatic": "^3.1.0",
|
|
34
34
|
"@types/d3-shape": "^3.1.8",
|
|
35
35
|
"@types/d3-time": "^3.0.4",
|
|
36
|
-
"@vitest/browser": "^4.1.
|
|
37
|
-
"@vitest/browser-playwright": "^4.1.
|
|
38
|
-
"@vitest/ui": "^4.1.
|
|
39
|
-
"playwright": "^1.
|
|
40
|
-
"prettier": "^3.8.
|
|
36
|
+
"@vitest/browser": "^4.1.4",
|
|
37
|
+
"@vitest/browser-playwright": "^4.1.4",
|
|
38
|
+
"@vitest/ui": "^4.1.4",
|
|
39
|
+
"playwright": "^1.59.1",
|
|
40
|
+
"prettier": "^3.8.2",
|
|
41
41
|
"prettier-plugin-svelte": "^3.5.1",
|
|
42
|
-
"svelte": "5.
|
|
43
|
-
"svelte-check": "^4.4.
|
|
44
|
-
"svelte2tsx": "^0.7.
|
|
42
|
+
"svelte": "5.55.3",
|
|
43
|
+
"svelte-check": "^4.4.6",
|
|
44
|
+
"svelte2tsx": "^0.7.53",
|
|
45
45
|
"tslib": "^2.8.1",
|
|
46
|
-
"typescript": "^
|
|
47
|
-
"vite": "^8.0.
|
|
48
|
-
"vitest": "^4.1.
|
|
46
|
+
"typescript": "^6.0.2",
|
|
47
|
+
"vite": "^8.0.8",
|
|
48
|
+
"vitest": "^4.1.4",
|
|
49
49
|
"vitest-browser-svelte": "^2.1.0"
|
|
50
50
|
},
|
|
51
51
|
"type": "module",
|