@opendata-ai/openchart-vanilla 8.0.0-rc.2 → 8.0.0-rc.21
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/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
- package/dist/chunk-DCODCLWI.js +234 -0
- package/dist/chunk-DCODCLWI.js.map +1 -0
- package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
- package/dist/chunk-EKAUQAAX.js.map +1 -0
- package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
- package/dist/chunk-KNT5AZ2O.js.map +1 -0
- package/dist/chunk-RM5XS6NR.js +113 -0
- package/dist/chunk-RM5XS6NR.js.map +1 -0
- package/dist/chunk-T32UHMZA.js +282 -0
- package/dist/chunk-T32UHMZA.js.map +1 -0
- package/dist/export-gif.d.ts +113 -0
- package/dist/export-gif.js +21 -0
- package/dist/export-gif.js.map +1 -0
- package/dist/index.d.ts +283 -24
- package/dist/index.js +2012 -763
- package/dist/index.js.map +1 -1
- package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
- package/dist/simulation-worker.js +83 -2
- package/dist/static.js +7 -69
- package/dist/static.js.map +1 -1
- package/dist/story/index.d.ts +3 -2
- package/dist/story/index.js +5 -3
- package/dist/story/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +14 -6
- package/dist/chunk-5EOV5JQZ.js.map +0 -1
- package/dist/chunk-ENXSUXC5.js.map +0 -1
- package/src/__test-fixtures__/dom.ts +0 -70
- package/src/__test-fixtures__/specs.ts +0 -214
- package/src/__tests__/animation.test.ts +0 -358
- package/src/__tests__/annotations-render.test.ts +0 -201
- package/src/__tests__/compound-labels.test.ts +0 -122
- package/src/__tests__/continuous-legend.test.ts +0 -91
- package/src/__tests__/crosshair.test.ts +0 -130
- package/src/__tests__/edit-events.test.ts +0 -902
- package/src/__tests__/events.test.ts +0 -387
- package/src/__tests__/export.test.ts +0 -241
- package/src/__tests__/font-race.test.ts +0 -199
- package/src/__tests__/gradient-ids.test.ts +0 -159
- package/src/__tests__/gradient-utils.test.ts +0 -125
- package/src/__tests__/known-bugs-render.test.ts +0 -183
- package/src/__tests__/map.test.ts +0 -293
- package/src/__tests__/mount.test.ts +0 -357
- package/src/__tests__/pattern-fills.test.ts +0 -105
- package/src/__tests__/resize-timing.test.ts +0 -218
- package/src/__tests__/sankey.test.ts +0 -133
- package/src/__tests__/selection-events.test.ts +0 -869
- package/src/__tests__/series-search.test.ts +0 -311
- package/src/__tests__/static.test.ts +0 -141
- package/src/__tests__/svg-renderer.test.ts +0 -895
- package/src/__tests__/table-keyboard.test.ts +0 -361
- package/src/__tests__/table-mount.test.ts +0 -639
- package/src/__tests__/tilemap.test.ts +0 -158
- package/src/__tests__/tooltip.test.ts +0 -337
- package/src/__tests__/transition.test.ts +0 -1777
- package/src/__tests__/you-draw-it.test.ts +0 -273
- package/src/animation-vars.ts +0 -42
- package/src/animation.ts +0 -76
- package/src/barlist-mount.ts +0 -383
- package/src/barlist-renderer.ts +0 -267
- package/src/dom-helpers.ts +0 -17
- package/src/export.ts +0 -482
- package/src/gradient-utils.ts +0 -138
- package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
- package/src/graph/__tests__/graph-mount.test.ts +0 -213
- package/src/graph/__tests__/interaction.test.ts +0 -205
- package/src/graph/__tests__/keyboard.test.ts +0 -653
- package/src/graph/__tests__/search.test.ts +0 -88
- package/src/graph/__tests__/simulation.test.ts +0 -233
- package/src/graph/__tests__/spatial-index.test.ts +0 -142
- package/src/graph/__tests__/zoom.test.ts +0 -203
- package/src/graph/canvas-renderer.ts +0 -747
- package/src/graph/interaction.ts +0 -367
- package/src/graph/keyboard.ts +0 -208
- package/src/graph/search.ts +0 -50
- package/src/graph/simulation-worker-url.ts +0 -12
- package/src/graph/simulation-worker.ts +0 -278
- package/src/graph/simulation.ts +0 -422
- package/src/graph/spatial-index.ts +0 -121
- package/src/graph/types.ts +0 -47
- package/src/graph/worker-protocol.ts +0 -70
- package/src/graph/zoom.ts +0 -134
- package/src/graph-mount.ts +0 -888
- package/src/index.ts +0 -76
- package/src/interactions/chart-events.ts +0 -139
- package/src/interactions/crosshair.ts +0 -233
- package/src/interactions/drag-handler.ts +0 -175
- package/src/interactions/editing-drags.ts +0 -515
- package/src/interactions/index.ts +0 -25
- package/src/interactions/keyboard-nav.ts +0 -111
- package/src/interactions/legend-interaction.ts +0 -46
- package/src/interactions/selection.ts +0 -280
- package/src/interactions/tooltip-events.ts +0 -72
- package/src/map-camera.ts +0 -88
- package/src/map-mount.ts +0 -701
- package/src/map-renderer.ts +0 -338
- package/src/map-transition.ts +0 -83
- package/src/measure-text.ts +0 -134
- package/src/mount.ts +0 -1382
- package/src/pattern-utils.ts +0 -110
- package/src/renderers/annotations.ts +0 -388
- package/src/renderers/axes.ts +0 -351
- package/src/renderers/brand.ts +0 -87
- package/src/renderers/chrome.ts +0 -196
- package/src/renderers/endpoint-labels.ts +0 -150
- package/src/renderers/legend.ts +0 -311
- package/src/renderers/marks.ts +0 -561
- package/src/renderers/metrics.ts +0 -63
- package/src/renderers/svg-dom.ts +0 -40
- package/src/renderers/table-cells.ts +0 -446
- package/src/resize-observer.ts +0 -46
- package/src/sankey-mount.ts +0 -622
- package/src/sankey-renderer.ts +0 -614
- package/src/series-search.ts +0 -392
- package/src/static.ts +0 -251
- package/src/story/__tests__/camera-math.test.ts +0 -145
- package/src/story/__tests__/create-chart-story.test.ts +0 -317
- package/src/story/__tests__/progress-math.test.ts +0 -95
- package/src/story/__tests__/scroll-driver.test.ts +0 -130
- package/src/story/__tests__/tween.test.ts +0 -159
- package/src/story/camera-math.ts +0 -125
- package/src/story/create-chart-story.ts +0 -353
- package/src/story/crossfade.ts +0 -70
- package/src/story/index.ts +0 -39
- package/src/story/progress-math.ts +0 -85
- package/src/story/resolve-camera-target.ts +0 -119
- package/src/story/scroll-driver.ts +0 -206
- package/src/story/story-camera.ts +0 -49
- package/src/story/tween.ts +0 -112
- package/src/story/types.ts +0 -76
- package/src/svg-ids.ts +0 -23
- package/src/svg-renderer.ts +0 -383
- package/src/table-keyboard.ts +0 -266
- package/src/table-mount.ts +0 -567
- package/src/table-renderer.ts +0 -414
- package/src/text-edit-overlay.ts +0 -255
- package/src/theme-tokens.ts +0 -51
- package/src/tilemap-mount.ts +0 -511
- package/src/tilemap-renderer.ts +0 -396
- package/src/tooltip.ts +0 -177
- package/src/transition.ts +0 -2816
- package/src/you-draw-it.ts +0 -480
- /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { createContainer } from '../__test-fixtures__/dom';
|
|
3
|
-
import { createTileMap } from '../tilemap-mount';
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Shared fixtures
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
const basicTileMapSpec = {
|
|
10
|
-
type: 'tilemap' as const,
|
|
11
|
-
data: { CA: 5.4, TX: 4.1, NY: 4.5, FL: 3.3, IL: 4.6 } as Record<string, number>,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Tests
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
describe('createTileMap', () => {
|
|
19
|
-
let container: HTMLDivElement;
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
container = createContainer();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
document.body.innerHTML = '';
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('mounts an SVG element into the container', () => {
|
|
30
|
-
const instance = createTileMap(container, basicTileMapSpec, { responsive: false });
|
|
31
|
-
|
|
32
|
-
const svg = container.querySelector('svg');
|
|
33
|
-
expect(svg).not.toBeNull();
|
|
34
|
-
expect(svg?.classList.contains('oc-tilemap')).toBe(true);
|
|
35
|
-
|
|
36
|
-
instance.destroy();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('renders all 51 state tiles', () => {
|
|
40
|
-
const instance = createTileMap(container, basicTileMapSpec, { responsive: false });
|
|
41
|
-
|
|
42
|
-
const tiles = container.querySelectorAll('.oc-tilemap-tile');
|
|
43
|
-
expect(tiles).toHaveLength(51);
|
|
44
|
-
|
|
45
|
-
instance.destroy();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('update() re-renders with new data', () => {
|
|
49
|
-
const instance = createTileMap(container, basicTileMapSpec, { responsive: false });
|
|
50
|
-
|
|
51
|
-
// Update with full data
|
|
52
|
-
const updatedSpec = {
|
|
53
|
-
...basicTileMapSpec,
|
|
54
|
-
data: {
|
|
55
|
-
AL: 2.7,
|
|
56
|
-
AK: 6.4,
|
|
57
|
-
AZ: 3.5,
|
|
58
|
-
AR: 3.4,
|
|
59
|
-
CA: 5.4,
|
|
60
|
-
CO: 3.4,
|
|
61
|
-
CT: 4.1,
|
|
62
|
-
DE: 4.4,
|
|
63
|
-
FL: 3.3,
|
|
64
|
-
GA: 3.4,
|
|
65
|
-
HI: 3.2,
|
|
66
|
-
ID: 3.0,
|
|
67
|
-
IL: 4.6,
|
|
68
|
-
IN: 3.3,
|
|
69
|
-
IA: 2.7,
|
|
70
|
-
KS: 3.2,
|
|
71
|
-
KY: 4.4,
|
|
72
|
-
LA: 3.6,
|
|
73
|
-
ME: 3.6,
|
|
74
|
-
MD: 1.8,
|
|
75
|
-
MA: 3.3,
|
|
76
|
-
MI: 4.2,
|
|
77
|
-
MN: 2.8,
|
|
78
|
-
MS: 3.7,
|
|
79
|
-
MO: 3.5,
|
|
80
|
-
MT: 2.9,
|
|
81
|
-
NE: 2.2,
|
|
82
|
-
NV: 5.4,
|
|
83
|
-
NH: 2.4,
|
|
84
|
-
NJ: 4.8,
|
|
85
|
-
NM: 4.1,
|
|
86
|
-
NY: 4.5,
|
|
87
|
-
NC: 3.5,
|
|
88
|
-
ND: 1.9,
|
|
89
|
-
OH: 4.0,
|
|
90
|
-
OK: 3.9,
|
|
91
|
-
OR: 4.2,
|
|
92
|
-
PA: 3.4,
|
|
93
|
-
RI: 3.8,
|
|
94
|
-
SC: 3.3,
|
|
95
|
-
SD: 2.0,
|
|
96
|
-
TN: 3.5,
|
|
97
|
-
TX: 4.1,
|
|
98
|
-
UT: 2.9,
|
|
99
|
-
VT: 2.3,
|
|
100
|
-
VA: 2.9,
|
|
101
|
-
WA: 4.6,
|
|
102
|
-
WV: 4.0,
|
|
103
|
-
WI: 2.9,
|
|
104
|
-
WY: 3.2,
|
|
105
|
-
DC: 5.2,
|
|
106
|
-
} as Record<string, number>,
|
|
107
|
-
};
|
|
108
|
-
instance.update(updatedSpec);
|
|
109
|
-
|
|
110
|
-
// All tiles should have data now
|
|
111
|
-
const dataTiles = instance.layout.tiles.filter((t) => t.hasData);
|
|
112
|
-
expect(dataTiles).toHaveLength(51);
|
|
113
|
-
|
|
114
|
-
instance.destroy();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('destroy() removes the SVG from the container', () => {
|
|
118
|
-
const instance = createTileMap(container, basicTileMapSpec, { responsive: false });
|
|
119
|
-
|
|
120
|
-
const svgBefore = container.querySelector('svg');
|
|
121
|
-
expect(svgBefore).not.toBeNull();
|
|
122
|
-
|
|
123
|
-
instance.destroy();
|
|
124
|
-
|
|
125
|
-
const svgAfter = container.querySelector('svg');
|
|
126
|
-
expect(svgAfter).toBeNull();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('gradient legend renders', () => {
|
|
130
|
-
const instance = createTileMap(container, basicTileMapSpec, { responsive: false });
|
|
131
|
-
|
|
132
|
-
const legend = container.querySelector('.oc-tilemap-legend');
|
|
133
|
-
expect(legend).not.toBeNull();
|
|
134
|
-
|
|
135
|
-
instance.destroy();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('layout property returns the compiled layout', () => {
|
|
139
|
-
const instance = createTileMap(container, basicTileMapSpec, { responsive: false });
|
|
140
|
-
|
|
141
|
-
expect(instance.layout).toBeDefined();
|
|
142
|
-
expect(instance.layout.tiles).toHaveLength(51);
|
|
143
|
-
expect(instance.layout.gradientLegend).not.toBeNull();
|
|
144
|
-
|
|
145
|
-
instance.destroy();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('export("svg") returns a string containing SVG content', () => {
|
|
149
|
-
const instance = createTileMap(container, basicTileMapSpec, { responsive: false });
|
|
150
|
-
|
|
151
|
-
const svgString = instance.export('svg');
|
|
152
|
-
expect(typeof svgString).toBe('string');
|
|
153
|
-
expect(svgString).toContain('<svg');
|
|
154
|
-
expect(svgString).toContain('oc-tilemap');
|
|
155
|
-
|
|
156
|
-
instance.destroy();
|
|
157
|
-
});
|
|
158
|
-
});
|
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tooltip manager tests.
|
|
3
|
-
*
|
|
4
|
-
* Tests the createTooltipManager() show/hide lifecycle, positioning logic,
|
|
5
|
-
* and cleanup behavior.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
-
import { createContainer } from '../__test-fixtures__/dom';
|
|
10
|
-
|
|
11
|
-
const mockComputePosition = vi.fn();
|
|
12
|
-
|
|
13
|
-
vi.mock('@floating-ui/dom', async (importOriginal) => {
|
|
14
|
-
const actual = await importOriginal<typeof import('@floating-ui/dom')>();
|
|
15
|
-
return {
|
|
16
|
-
...actual,
|
|
17
|
-
computePosition: (...args: unknown[]) => mockComputePosition(...args),
|
|
18
|
-
};
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// Import after mock so the module picks up the mock
|
|
22
|
-
const { createTooltipManager } = await import('../tooltip');
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Flush the microtask queue so computePosition's .then() resolves.
|
|
26
|
-
*/
|
|
27
|
-
const flushPositioning = () => vi.waitFor(() => Promise.resolve());
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Cleanup
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
const defaultPositionResult = {
|
|
34
|
-
x: 0,
|
|
35
|
-
y: 0,
|
|
36
|
-
placement: 'bottom-start' as const,
|
|
37
|
-
strategy: 'absolute' as const,
|
|
38
|
-
middlewareData: {},
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
beforeEach(() => {
|
|
42
|
-
mockComputePosition.mockResolvedValue(defaultPositionResult);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
afterEach(() => {
|
|
46
|
-
document.body.innerHTML = '';
|
|
47
|
-
mockComputePosition.mockReset();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// Lifecycle
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
describe('createTooltipManager lifecycle', () => {
|
|
55
|
-
it('appends a tooltip element to the container on creation', () => {
|
|
56
|
-
const container = createContainer();
|
|
57
|
-
createTooltipManager(container);
|
|
58
|
-
|
|
59
|
-
const tooltip = container.querySelector('.oc-tooltip');
|
|
60
|
-
expect(tooltip).not.toBeNull();
|
|
61
|
-
expect(tooltip!.getAttribute('role')).toBe('tooltip');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('show() makes the tooltip visible', () => {
|
|
65
|
-
const container = createContainer();
|
|
66
|
-
const manager = createTooltipManager(container);
|
|
67
|
-
|
|
68
|
-
manager.show({ title: 'Point A', fields: [{ label: 'Value', value: '42' }] }, 100, 100);
|
|
69
|
-
|
|
70
|
-
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
71
|
-
expect(tooltip.style.display).toBe('block');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('show() renders title and field content', () => {
|
|
75
|
-
const container = createContainer();
|
|
76
|
-
const manager = createTooltipManager(container);
|
|
77
|
-
|
|
78
|
-
manager.show(
|
|
79
|
-
{
|
|
80
|
-
title: '2021-Q1',
|
|
81
|
-
fields: [
|
|
82
|
-
{ label: 'Revenue', value: '$1.2M', color: '#3b82f6' },
|
|
83
|
-
{ label: 'Growth', value: '+15%' },
|
|
84
|
-
],
|
|
85
|
-
},
|
|
86
|
-
100,
|
|
87
|
-
100,
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
91
|
-
expect(tooltip.innerHTML).toContain('2021-Q1');
|
|
92
|
-
expect(tooltip.innerHTML).toContain('Revenue');
|
|
93
|
-
expect(tooltip.innerHTML).toContain('$1.2M');
|
|
94
|
-
expect(tooltip.innerHTML).toContain('Growth');
|
|
95
|
-
expect(tooltip.innerHTML).toContain('+15%');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('show() renders color dot when field has color', () => {
|
|
99
|
-
const container = createContainer();
|
|
100
|
-
const manager = createTooltipManager(container);
|
|
101
|
-
|
|
102
|
-
manager.show(
|
|
103
|
-
{
|
|
104
|
-
title: 'Test',
|
|
105
|
-
fields: [{ label: 'Value', value: '42', color: '#ff0000' }],
|
|
106
|
-
},
|
|
107
|
-
50,
|
|
108
|
-
50,
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
const dot = container.querySelector('.oc-tooltip-dot') as HTMLElement;
|
|
112
|
-
expect(dot).not.toBeNull();
|
|
113
|
-
expect(dot.style.background).toBe('#ff0000');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('hide() hides the tooltip', () => {
|
|
117
|
-
const container = createContainer();
|
|
118
|
-
const manager = createTooltipManager(container);
|
|
119
|
-
|
|
120
|
-
manager.show({ title: 'Test', fields: [{ label: 'V', value: '1' }] }, 100, 100);
|
|
121
|
-
manager.hide();
|
|
122
|
-
|
|
123
|
-
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
124
|
-
expect(tooltip.style.display).toBe('none');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('Escape dismisses a visible tooltip without pointer movement (WCAG 1.4.13)', () => {
|
|
128
|
-
const container = createContainer();
|
|
129
|
-
const manager = createTooltipManager(container);
|
|
130
|
-
|
|
131
|
-
manager.show({ title: 'Test', fields: [{ label: 'V', value: '1' }] }, 100, 100);
|
|
132
|
-
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
133
|
-
|
|
134
|
-
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
135
|
-
expect(tooltip.style.display).toBe('none');
|
|
136
|
-
manager.destroy();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('destroy() removes the Escape listener', () => {
|
|
140
|
-
const container = createContainer();
|
|
141
|
-
const manager = createTooltipManager(container);
|
|
142
|
-
|
|
143
|
-
manager.show({ title: 'Test', fields: [{ label: 'V', value: '1' }] }, 100, 100);
|
|
144
|
-
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
145
|
-
manager.destroy();
|
|
146
|
-
|
|
147
|
-
// Dispatch after destroy: no listener should touch the detached element
|
|
148
|
-
tooltip.style.display = 'block';
|
|
149
|
-
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
150
|
-
expect(tooltip.style.display).toBe('block');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('destroy() removes the tooltip element from the DOM', () => {
|
|
154
|
-
const container = createContainer();
|
|
155
|
-
const manager = createTooltipManager(container);
|
|
156
|
-
|
|
157
|
-
// Verify it exists
|
|
158
|
-
expect(container.querySelector('.oc-tooltip')).not.toBeNull();
|
|
159
|
-
|
|
160
|
-
manager.destroy();
|
|
161
|
-
|
|
162
|
-
expect(container.querySelector('.oc-tooltip')).toBeNull();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('show() updates content when called again', () => {
|
|
166
|
-
const container = createContainer();
|
|
167
|
-
const manager = createTooltipManager(container);
|
|
168
|
-
|
|
169
|
-
manager.show({ title: 'First', fields: [{ label: 'A', value: '1' }] }, 50, 50);
|
|
170
|
-
|
|
171
|
-
let tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
172
|
-
expect(tooltip.innerHTML).toContain('First');
|
|
173
|
-
|
|
174
|
-
manager.show({ title: 'Second', fields: [{ label: 'B', value: '2' }] }, 100, 100);
|
|
175
|
-
|
|
176
|
-
tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
177
|
-
expect(tooltip.innerHTML).toContain('Second');
|
|
178
|
-
// First content should be replaced
|
|
179
|
-
expect(tooltip.innerHTML).not.toContain('First');
|
|
180
|
-
|
|
181
|
-
manager.destroy();
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
|
-
// Positioning
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
|
|
189
|
-
describe('tooltip positioning', () => {
|
|
190
|
-
it('positions tooltip via computePosition with flip and shift', async () => {
|
|
191
|
-
mockComputePosition.mockResolvedValueOnce({
|
|
192
|
-
x: 112,
|
|
193
|
-
y: 212,
|
|
194
|
-
placement: 'bottom-start',
|
|
195
|
-
strategy: 'absolute',
|
|
196
|
-
middlewareData: {},
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const container = createContainer();
|
|
200
|
-
const manager = createTooltipManager(container);
|
|
201
|
-
|
|
202
|
-
manager.show({ title: 'Test', fields: [{ label: 'V', value: '1' }] }, 100, 200);
|
|
203
|
-
await flushPositioning();
|
|
204
|
-
|
|
205
|
-
// computePosition should have been called with the tooltip element
|
|
206
|
-
expect(mockComputePosition).toHaveBeenCalledOnce();
|
|
207
|
-
const [ref, tooltipEl, options] = mockComputePosition.mock.calls[0];
|
|
208
|
-
expect(tooltipEl).toBeInstanceOf(HTMLElement);
|
|
209
|
-
expect(options?.placement).toBe('bottom-start');
|
|
210
|
-
// Should include offset, flip, and shift middleware
|
|
211
|
-
expect(options?.middleware).toHaveLength(3);
|
|
212
|
-
|
|
213
|
-
// Virtual reference should return a rect at the mouse position
|
|
214
|
-
const rect = (ref as { getBoundingClientRect: () => DOMRect }).getBoundingClientRect();
|
|
215
|
-
expect(rect.width).toBe(0);
|
|
216
|
-
expect(rect.height).toBe(0);
|
|
217
|
-
|
|
218
|
-
// Position should be applied from computePosition result
|
|
219
|
-
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
220
|
-
expect(tooltip.style.left).toContain('px');
|
|
221
|
-
expect(tooltip.style.top).toContain('px');
|
|
222
|
-
|
|
223
|
-
manager.destroy();
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('discards stale position callbacks on rapid show() calls', async () => {
|
|
227
|
-
type PosResult = {
|
|
228
|
-
x: number;
|
|
229
|
-
y: number;
|
|
230
|
-
placement: string;
|
|
231
|
-
strategy: string;
|
|
232
|
-
middlewareData: Record<string, never>;
|
|
233
|
-
};
|
|
234
|
-
let resolveFirst!: (val: PosResult) => void;
|
|
235
|
-
let resolveSecond!: (val: PosResult) => void;
|
|
236
|
-
|
|
237
|
-
mockComputePosition
|
|
238
|
-
.mockImplementationOnce(
|
|
239
|
-
() =>
|
|
240
|
-
new Promise((r) => {
|
|
241
|
-
resolveFirst = r;
|
|
242
|
-
}),
|
|
243
|
-
)
|
|
244
|
-
.mockImplementationOnce(
|
|
245
|
-
() =>
|
|
246
|
-
new Promise((r) => {
|
|
247
|
-
resolveSecond = r;
|
|
248
|
-
}),
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
const container = createContainer();
|
|
252
|
-
const manager = createTooltipManager(container);
|
|
253
|
-
|
|
254
|
-
// Two rapid show() calls
|
|
255
|
-
manager.show({ title: 'A', fields: [{ label: 'V', value: '1' }] }, 10, 10);
|
|
256
|
-
manager.show({ title: 'B', fields: [{ label: 'V', value: '2' }] }, 200, 200);
|
|
257
|
-
|
|
258
|
-
// Resolve the second (latest) first
|
|
259
|
-
resolveSecond({
|
|
260
|
-
x: 212,
|
|
261
|
-
y: 212,
|
|
262
|
-
placement: 'bottom-start',
|
|
263
|
-
strategy: 'absolute',
|
|
264
|
-
middlewareData: {},
|
|
265
|
-
});
|
|
266
|
-
await flushPositioning();
|
|
267
|
-
|
|
268
|
-
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
269
|
-
const posAfterSecond = tooltip.style.left;
|
|
270
|
-
|
|
271
|
-
// Now resolve the first (stale) - should be discarded
|
|
272
|
-
resolveFirst({
|
|
273
|
-
x: 22,
|
|
274
|
-
y: 22,
|
|
275
|
-
placement: 'bottom-start',
|
|
276
|
-
strategy: 'absolute',
|
|
277
|
-
middlewareData: {},
|
|
278
|
-
});
|
|
279
|
-
await flushPositioning();
|
|
280
|
-
|
|
281
|
-
// Position should not have changed (stale callback was discarded)
|
|
282
|
-
expect(tooltip.style.left).toBe(posAfterSecond);
|
|
283
|
-
|
|
284
|
-
manager.destroy();
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('sets container position to relative if not already set', () => {
|
|
288
|
-
const container = createContainer();
|
|
289
|
-
container.style.position = '';
|
|
290
|
-
|
|
291
|
-
createTooltipManager(container);
|
|
292
|
-
|
|
293
|
-
// createTooltipManager sets container.style.position to 'relative'
|
|
294
|
-
// if it was empty
|
|
295
|
-
expect(container.style.position).toBe('relative');
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it('preserves existing container position style', () => {
|
|
299
|
-
const container = createContainer();
|
|
300
|
-
container.style.position = 'absolute';
|
|
301
|
-
|
|
302
|
-
createTooltipManager(container);
|
|
303
|
-
|
|
304
|
-
expect(container.style.position).toBe('absolute');
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
// ---------------------------------------------------------------------------
|
|
309
|
-
// HTML escaping
|
|
310
|
-
// ---------------------------------------------------------------------------
|
|
311
|
-
|
|
312
|
-
describe('tooltip content escaping', () => {
|
|
313
|
-
it('escapes HTML special characters in title and fields', () => {
|
|
314
|
-
const container = createContainer();
|
|
315
|
-
const manager = createTooltipManager(container);
|
|
316
|
-
|
|
317
|
-
manager.show(
|
|
318
|
-
{
|
|
319
|
-
title: '<script>alert("xss")</script>',
|
|
320
|
-
fields: [{ label: 'A&B', value: 'val<ue' }],
|
|
321
|
-
},
|
|
322
|
-
50,
|
|
323
|
-
50,
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
const tooltip = container.querySelector('.oc-tooltip') as HTMLElement;
|
|
327
|
-
// Should not contain raw HTML tags - the <script> should be escaped
|
|
328
|
-
expect(tooltip.innerHTML).not.toContain('<script>');
|
|
329
|
-
// Should contain escaped versions of angle brackets and ampersands
|
|
330
|
-
expect(tooltip.innerHTML).toContain('<script>');
|
|
331
|
-
expect(tooltip.innerHTML).toContain('A&B');
|
|
332
|
-
// The value with < should be escaped
|
|
333
|
-
expect(tooltip.innerHTML).toContain('val<ue');
|
|
334
|
-
|
|
335
|
-
manager.destroy();
|
|
336
|
-
});
|
|
337
|
-
});
|