@opendata-ai/openchart-vanilla 7.10.0 → 7.11.0
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 +47 -5
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__test-fixtures__/dom.ts +28 -0
- package/src/__test-fixtures__/specs.ts +27 -0
- package/src/__tests__/mount.test.ts +95 -2
- package/src/__tests__/resize-timing.test.ts +36 -2
- package/src/mount.ts +119 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-vanilla",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.11.0",
|
|
4
4
|
"description": "Vanilla JS renderer for openchart: SVG charts, HTML tables, force-directed graphs",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Riley Hilliard",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@floating-ui/dom": "^1.7.6",
|
|
57
|
-
"@opendata-ai/openchart-core": "7.
|
|
58
|
-
"@opendata-ai/openchart-engine": "7.
|
|
57
|
+
"@opendata-ai/openchart-core": "7.11.0",
|
|
58
|
+
"@opendata-ai/openchart-engine": "7.11.0",
|
|
59
59
|
"d3-force": "^3.0.0",
|
|
60
60
|
"d3-quadtree": "^3.0.1"
|
|
61
61
|
},
|
|
@@ -29,6 +29,34 @@ export function createContainer(width = 600, height = 400): HTMLDivElement {
|
|
|
29
29
|
return container;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Create an auto-height container: its measured height is 0 until a chart
|
|
34
|
+
* SVG is mounted, then tracks the SVG's own style.height — mimicking a real
|
|
35
|
+
* DOM container with no explicit height that wraps its content.
|
|
36
|
+
*/
|
|
37
|
+
export function createAutoHeightContainer(width = 390): HTMLDivElement {
|
|
38
|
+
const container = document.createElement('div');
|
|
39
|
+
Object.defineProperty(container, 'getBoundingClientRect', {
|
|
40
|
+
value: () => {
|
|
41
|
+
const svg = container.querySelector('svg');
|
|
42
|
+
const height = svg ? Number.parseFloat((svg as SVGElement).style.height) || 0 : 0;
|
|
43
|
+
return {
|
|
44
|
+
width,
|
|
45
|
+
height,
|
|
46
|
+
top: 0,
|
|
47
|
+
left: 0,
|
|
48
|
+
right: width,
|
|
49
|
+
bottom: height,
|
|
50
|
+
x: 0,
|
|
51
|
+
y: 0,
|
|
52
|
+
toJSON: () => ({}),
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
document.body.appendChild(container);
|
|
57
|
+
return container;
|
|
58
|
+
}
|
|
59
|
+
|
|
32
60
|
/**
|
|
33
61
|
* Create a MouseEvent with clientX/clientY coordinates.
|
|
34
62
|
* happy-dom supports basic MouseEvent construction.
|
|
@@ -31,6 +31,33 @@ export const lineSpec: ChartSpec = {
|
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Multi-series line with wrapping chrome and an explicit top legend — the
|
|
36
|
+
* mobile auto-height growth scenario (long title + subtitle + legend +
|
|
37
|
+
* source all stack above/below the plot).
|
|
38
|
+
*/
|
|
39
|
+
export const longChromeLineSpec: ChartSpec = {
|
|
40
|
+
mark: 'line',
|
|
41
|
+
data: [
|
|
42
|
+
{ date: '2020-01-01', value: 10, country: 'US' },
|
|
43
|
+
{ date: '2021-01-01', value: 40, country: 'US' },
|
|
44
|
+
{ date: '2020-01-01', value: 15, country: 'UK' },
|
|
45
|
+
{ date: '2021-01-01', value: 35, country: 'UK' },
|
|
46
|
+
],
|
|
47
|
+
encoding: {
|
|
48
|
+
x: { field: 'date', type: 'temporal' },
|
|
49
|
+
y: { field: 'value', type: 'quantitative' },
|
|
50
|
+
color: { field: 'country', type: 'nominal' },
|
|
51
|
+
},
|
|
52
|
+
legend: { show: true, position: 'top' },
|
|
53
|
+
chrome: {
|
|
54
|
+
title:
|
|
55
|
+
'An intentionally long editorial chart title that wraps to several lines at phone widths',
|
|
56
|
+
subtitle: 'A methodological subtitle that provides additional context for readers',
|
|
57
|
+
source: 'World Bank',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
34
61
|
export const singleSeriesLineSpec: ChartSpec = {
|
|
35
62
|
mark: 'line',
|
|
36
63
|
data: [
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import type { ChartLayout } from '@opendata-ai/openchart-core';
|
|
2
|
+
import { legendGap } from '@opendata-ai/openchart-engine';
|
|
1
3
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { createContainer } from '../__test-fixtures__/dom';
|
|
3
|
-
import {
|
|
4
|
+
import { createAutoHeightContainer, createContainer } from '../__test-fixtures__/dom';
|
|
5
|
+
import {
|
|
6
|
+
barSpec,
|
|
7
|
+
lineSpec,
|
|
8
|
+
longChromeLineSpec,
|
|
9
|
+
singleSeriesLineSpec,
|
|
10
|
+
} from '../__test-fixtures__/specs';
|
|
4
11
|
import { createChart } from '../mount';
|
|
5
12
|
|
|
6
13
|
// ---------------------------------------------------------------------------
|
|
@@ -236,3 +243,89 @@ describe('resize observer integration', () => {
|
|
|
236
243
|
chart.destroy();
|
|
237
244
|
});
|
|
238
245
|
});
|
|
246
|
+
|
|
247
|
+
describe('auto-height container growth', () => {
|
|
248
|
+
afterEach(() => {
|
|
249
|
+
document.body.innerHTML = '';
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
/** The chrome/legend/metrics blocks the mount grows the figure by. */
|
|
253
|
+
function overheadsOf(layout: ChartLayout, width: number): number {
|
|
254
|
+
const topLegendBlock =
|
|
255
|
+
layout.legend.position === 'top' &&
|
|
256
|
+
'entries' in layout.legend &&
|
|
257
|
+
layout.legend.entries.length > 0
|
|
258
|
+
? layout.legend.bounds.height + legendGap(width)
|
|
259
|
+
: 0;
|
|
260
|
+
return (
|
|
261
|
+
layout.chrome.topHeight +
|
|
262
|
+
layout.chrome.bottomHeight +
|
|
263
|
+
topLegendBlock +
|
|
264
|
+
(layout.metrics?.height ?? 0)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
it('grows the figure beyond the 400px budget by the measured overheads', () => {
|
|
269
|
+
const container = createAutoHeightContainer(390);
|
|
270
|
+
const chart = createChart(container, longChromeLineSpec);
|
|
271
|
+
|
|
272
|
+
const height = chart.layout.dimensions.height;
|
|
273
|
+
expect(height).toBeGreaterThan(400);
|
|
274
|
+
expect(height).toBeCloseTo(400 + overheadsOf(chart.layout, 390), 5);
|
|
275
|
+
|
|
276
|
+
// The rendered SVG is sized to the grown figure, not the 400 fallback.
|
|
277
|
+
const svg = container.querySelector('svg');
|
|
278
|
+
expect(svg?.getAttribute('viewBox')).toBe(`0 0 390 ${height}`);
|
|
279
|
+
expect((svg as SVGElement).style.height).toBe(`${height}px`);
|
|
280
|
+
|
|
281
|
+
chart.destroy();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('keeps explicit-height containers at the host height', () => {
|
|
285
|
+
const container = createContainer(390, 500);
|
|
286
|
+
const chart = createChart(container, longChromeLineSpec);
|
|
287
|
+
|
|
288
|
+
// Host constrains the box; chrome must fit inside it, not grow it.
|
|
289
|
+
expect(chart.layout.dimensions.height).toBe(500);
|
|
290
|
+
|
|
291
|
+
chart.destroy();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('re-renders against the 400 budget, not the grown height (no re-pin)', () => {
|
|
295
|
+
const container = createAutoHeightContainer(390);
|
|
296
|
+
const chart = createChart(container, longChromeLineSpec);
|
|
297
|
+
|
|
298
|
+
const grownHeight = chart.layout.dimensions.height;
|
|
299
|
+
expect(grownHeight).toBeGreaterThan(400);
|
|
300
|
+
|
|
301
|
+
// Same spec again: the container now measures at the grown SVG height,
|
|
302
|
+
// but the latched-auto mount keeps compiling against the 400 budget, so
|
|
303
|
+
// the figure height is stable.
|
|
304
|
+
chart.update(longChromeLineSpec);
|
|
305
|
+
expect(chart.layout.dimensions.height).toBe(grownHeight);
|
|
306
|
+
|
|
307
|
+
// Shorter chrome: the figure must shrink toward the budget instead of
|
|
308
|
+
// staying pinned at the previously grown container height.
|
|
309
|
+
chart.update(singleSeriesLineSpec);
|
|
310
|
+
const shrunkHeight = chart.layout.dimensions.height;
|
|
311
|
+
expect(shrunkHeight).toBeLessThan(grownHeight);
|
|
312
|
+
expect(shrunkHeight).toBeCloseTo(400 + overheadsOf(chart.layout, 390), 5);
|
|
313
|
+
|
|
314
|
+
chart.destroy();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('does not grow sparkline mounts', () => {
|
|
318
|
+
const container = createAutoHeightContainer(200);
|
|
319
|
+
const chart = createChart(container, {
|
|
320
|
+
mark: 'line',
|
|
321
|
+
data: lineSpec.data,
|
|
322
|
+
encoding: lineSpec.encoding,
|
|
323
|
+
display: 'sparkline',
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Sparkline auto-height fallback stays at the tiny 40px default.
|
|
327
|
+
expect(chart.layout.dimensions.height).toBe(40);
|
|
328
|
+
|
|
329
|
+
chart.destroy();
|
|
330
|
+
});
|
|
331
|
+
});
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
17
|
-
import { createContainer } from '../__test-fixtures__/dom';
|
|
18
|
-
import { lineSpec } from '../__test-fixtures__/specs';
|
|
17
|
+
import { createAutoHeightContainer, createContainer } from '../__test-fixtures__/dom';
|
|
18
|
+
import { lineSpec, longChromeLineSpec } from '../__test-fixtures__/specs';
|
|
19
19
|
import { createChart } from '../mount';
|
|
20
20
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
@@ -181,4 +181,38 @@ describe('chart resize timing', () => {
|
|
|
181
181
|
// try to render a new one. No SVG should be present.
|
|
182
182
|
expect(container.querySelector('svg')).toBeNull();
|
|
183
183
|
});
|
|
184
|
+
|
|
185
|
+
describe('auto-height observer feedback guard', () => {
|
|
186
|
+
it('ignores the observer echo from the figure growing itself', async () => {
|
|
187
|
+
const autoContainer = createAutoHeightContainer(390);
|
|
188
|
+
const chart = createChart(autoContainer, longChromeLineSpec);
|
|
189
|
+
|
|
190
|
+
const grownHeight = chart.layout.dimensions.height;
|
|
191
|
+
expect(grownHeight).toBeGreaterThan(400);
|
|
192
|
+
const genBefore = autoContainer.dataset.ocRenderGen;
|
|
193
|
+
|
|
194
|
+
// Growing the SVG grows the container, which fires the observer with
|
|
195
|
+
// our own dimensions. That echo must not trigger a re-render (the
|
|
196
|
+
// re-render would grow again and loop).
|
|
197
|
+
fireResize(autoContainer, 390, grownHeight);
|
|
198
|
+
await vi.runAllTimersAsync();
|
|
199
|
+
expect(autoContainer.dataset.ocRenderGen).toBe(genBefore);
|
|
200
|
+
|
|
201
|
+
chart.destroy();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('still re-renders when the width changes', async () => {
|
|
205
|
+
const autoContainer = createAutoHeightContainer(390);
|
|
206
|
+
const chart = createChart(autoContainer, longChromeLineSpec);
|
|
207
|
+
|
|
208
|
+
const grownHeight = chart.layout.dimensions.height;
|
|
209
|
+
const genBefore = autoContainer.dataset.ocRenderGen;
|
|
210
|
+
|
|
211
|
+
fireResize(autoContainer, 340, grownHeight);
|
|
212
|
+
await vi.runAllTimersAsync();
|
|
213
|
+
expect(autoContainer.dataset.ocRenderGen).not.toBe(genBefore);
|
|
214
|
+
|
|
215
|
+
chart.destroy();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
184
218
|
});
|
package/src/mount.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
ThemeConfig,
|
|
22
22
|
} from '@opendata-ai/openchart-core';
|
|
23
23
|
import { isGraphSpec, isLayerSpec } from '@opendata-ai/openchart-core';
|
|
24
|
-
import { compileChart, compileLayer } from '@opendata-ai/openchart-engine';
|
|
24
|
+
import { compileChart, compileLayer, legendGap } from '@opendata-ai/openchart-engine';
|
|
25
25
|
import { cancelAnimations, setupAnimationCleanup } from './animation';
|
|
26
26
|
import {
|
|
27
27
|
exportCSV,
|
|
@@ -120,6 +120,18 @@ export interface ChartInstance {
|
|
|
120
120
|
setHighlight(values: string[] | null): void;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Container sizing
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fallback height when the container doesn't constrain height. For
|
|
129
|
+
* auto-height ChartSpec mounts this is the fixed budget for the
|
|
130
|
+
* visualization itself (padding + axes + plot); chrome, the top legend
|
|
131
|
+
* block, and the metrics bar grow the figure on top of it.
|
|
132
|
+
*/
|
|
133
|
+
const FALLBACK_HEIGHT = 400;
|
|
134
|
+
|
|
123
135
|
// ---------------------------------------------------------------------------
|
|
124
136
|
// Dark mode resolution
|
|
125
137
|
// ---------------------------------------------------------------------------
|
|
@@ -194,6 +206,19 @@ export function createChart<TData extends DataRow = DataRow>(
|
|
|
194
206
|
let cleanupAnimations: (() => void) | null = null;
|
|
195
207
|
let pendingResize = false;
|
|
196
208
|
|
|
209
|
+
// Auto-height sizing state. `isAutoHeight` latches whether the host
|
|
210
|
+
// constrains the container's height:
|
|
211
|
+
// null = unknown — every measurement so far was an all-zero rect
|
|
212
|
+
// (display:none / detached); keep re-checking each measure.
|
|
213
|
+
// true = auto-height — the figure grows with chrome (ChartSpec only).
|
|
214
|
+
// false = explicit height — fit inside the host's box (today's behavior).
|
|
215
|
+
// The latch matters because the SVG's own style.height gives the container
|
|
216
|
+
// a nonzero rect after the first render, so auto-height cannot be
|
|
217
|
+
// re-derived per measure.
|
|
218
|
+
let isAutoHeight: boolean | null = null;
|
|
219
|
+
let lastRenderedWidth: number | null = null;
|
|
220
|
+
let lastRenderedSvgHeight: number | null = null;
|
|
221
|
+
|
|
197
222
|
// Data-update transition state
|
|
198
223
|
let transitionHandle: import('./transition').TransitionHandle | null = null;
|
|
199
224
|
let transitionSnapshot: GeometrySnapshot | null = null;
|
|
@@ -239,8 +264,7 @@ export function createChart<TData extends DataRow = DataRow>(
|
|
|
239
264
|
// Compilation
|
|
240
265
|
// ---------------------------------------------------------------------------
|
|
241
266
|
|
|
242
|
-
function
|
|
243
|
-
const { width, height } = getContainerDimensions();
|
|
267
|
+
function compileAt(width: number, height: number): ChartLayout {
|
|
244
268
|
const darkMode = resolveDarkMode(options?.darkMode);
|
|
245
269
|
|
|
246
270
|
const compileOpts: CompileOptions = {
|
|
@@ -258,6 +282,61 @@ export function createChart<TData extends DataRow = DataRow>(
|
|
|
258
282
|
return compileChart(withRuntimeHidden(currentSpec) as ChartSpec | GraphSpec, compileOpts);
|
|
259
283
|
}
|
|
260
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Vertical overheads that grow an auto-height figure beyond the
|
|
287
|
+
* FALLBACK_HEIGHT budget: top+bottom chrome (bottom chrome already includes
|
|
288
|
+
* a bottom legend's reservation), the top legend block (height + gap), and
|
|
289
|
+
* the KPI metrics bar. topPad and the axis gap are plot clearance, not
|
|
290
|
+
* chrome — they stay inside the budget.
|
|
291
|
+
*/
|
|
292
|
+
function verticalOverheads(layout: ChartLayout, width: number): number {
|
|
293
|
+
const topLegendBlock =
|
|
294
|
+
layout.legend.position === 'top' &&
|
|
295
|
+
'entries' in layout.legend &&
|
|
296
|
+
layout.legend.entries.length > 0
|
|
297
|
+
? layout.legend.bounds.height + legendGap(width)
|
|
298
|
+
: 0;
|
|
299
|
+
return (
|
|
300
|
+
layout.chrome.topHeight +
|
|
301
|
+
layout.chrome.bottomHeight +
|
|
302
|
+
topLegendBlock +
|
|
303
|
+
(layout.metrics?.height ?? 0)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function compile(): ChartLayout {
|
|
308
|
+
const { width, height } = getContainerDimensions();
|
|
309
|
+
|
|
310
|
+
// Auto-height growth applies to ChartSpec mounts only. GraphSpec,
|
|
311
|
+
// LayerSpec, and sparkline mounts keep the fixed-fallback behavior.
|
|
312
|
+
const growsWithChrome =
|
|
313
|
+
isAutoHeight === true &&
|
|
314
|
+
!isLayerSpec(currentSpec) &&
|
|
315
|
+
!isGraphSpec(currentSpec as unknown as Record<string, unknown>) &&
|
|
316
|
+
(currentSpec as ChartSpec).display !== 'sparkline';
|
|
317
|
+
if (!growsWithChrome) {
|
|
318
|
+
return compileAt(width, height);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Bounded convergence loop, max 3 compiles. Chrome height depends on
|
|
322
|
+
// width (text wrap), not height — but the engine's chrome-strip guardrail
|
|
323
|
+
// strips chrome when the residual plot is too small, which makes chrome
|
|
324
|
+
// height-dependent at the budget boundary: growing the figure can
|
|
325
|
+
// un-strip chrome, changing the overheads once more. (Height classes are
|
|
326
|
+
// not the issue — they only strip below 200/350px, and growing from 400
|
|
327
|
+
// moves away from those thresholds.) Pass 3 is accepted unconditionally.
|
|
328
|
+
let layout = compileAt(width, height);
|
|
329
|
+
const overheads = verticalOverheads(layout, width);
|
|
330
|
+
if (overheads > 0) {
|
|
331
|
+
layout = compileAt(width, height + overheads);
|
|
332
|
+
const overheads2 = verticalOverheads(layout, width);
|
|
333
|
+
if (overheads2 !== overheads) {
|
|
334
|
+
layout = compileAt(width, height + overheads2);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return layout;
|
|
338
|
+
}
|
|
339
|
+
|
|
261
340
|
function withRuntimeHidden<T extends { hiddenSeries?: string[]; annotations?: Annotation[] }>(
|
|
262
341
|
spec: T,
|
|
263
342
|
): T {
|
|
@@ -366,9 +445,21 @@ export function createChart<TData extends DataRow = DataRow>(
|
|
|
366
445
|
height: Math.max(height || 40, 20),
|
|
367
446
|
};
|
|
368
447
|
}
|
|
448
|
+
// Latch auto-height on the first NONZERO measurement: height 0 with a
|
|
449
|
+
// real width means the host doesn't constrain height. An all-zero rect
|
|
450
|
+
// (display:none / detached) stays "unknown" — keep re-checking until the
|
|
451
|
+
// container produces a real measurement.
|
|
452
|
+
if (isAutoHeight === null && (rect.width > 0 || rect.height > 0)) {
|
|
453
|
+
isAutoHeight = rect.height === 0 && rect.width > 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
369
456
|
return {
|
|
370
457
|
width: Math.max(rect.width || 600, 100),
|
|
371
|
-
|
|
458
|
+
// Latched-auto mounts always compile against the fixed budget: after
|
|
459
|
+
// the first render the container's measured height is just our own
|
|
460
|
+
// SVG, so reading it back would re-pin whatever we rendered last.
|
|
461
|
+
height:
|
|
462
|
+
isAutoHeight === true ? FALLBACK_HEIGHT : Math.max(rect.height || FALLBACK_HEIGHT, 100),
|
|
372
463
|
};
|
|
373
464
|
}
|
|
374
465
|
|
|
@@ -676,6 +767,9 @@ export function createChart<TData extends DataRow = DataRow>(
|
|
|
676
767
|
}
|
|
677
768
|
|
|
678
769
|
currentLayout = compile();
|
|
770
|
+
// Recorded for the ResizeObserver's self-induced-resize guard.
|
|
771
|
+
lastRenderedWidth = currentLayout.dimensions.width;
|
|
772
|
+
lastRenderedSvgHeight = currentLayout.dimensions.height;
|
|
679
773
|
const shouldAnimate = isFirstRender && !!currentLayout.animation?.enter;
|
|
680
774
|
const crosshair = !!currentLayout.crosshair;
|
|
681
775
|
svgElement = renderChartSVG(currentLayout, container, {
|
|
@@ -1050,7 +1144,27 @@ export function createChart<TData extends DataRow = DataRow>(
|
|
|
1050
1144
|
|
|
1051
1145
|
// Set up responsive resize
|
|
1052
1146
|
if (options?.responsive !== false) {
|
|
1053
|
-
disconnectResize = observeResize(container, () => {
|
|
1147
|
+
disconnectResize = observeResize(container, (width, height) => {
|
|
1148
|
+
// Self-induced-resize guard, scoped to latched-auto mounts: growing
|
|
1149
|
+
// the SVG grows the container, which re-fires this observer. A fire
|
|
1150
|
+
// whose height matches the SVG we just rendered (and whose width is
|
|
1151
|
+
// unchanged) is our own echo — re-rendering on it would loop; the
|
|
1152
|
+
// 16ms debounce alone doesn't break the cycle.
|
|
1153
|
+
if (isAutoHeight === true) {
|
|
1154
|
+
const widthChanged = width !== lastRenderedWidth;
|
|
1155
|
+
const selfInducedHeight = height === lastRenderedSvgHeight;
|
|
1156
|
+
if (!widthChanged && selfInducedHeight) return;
|
|
1157
|
+
if (!selfInducedHeight) {
|
|
1158
|
+
// Any other height delta is host-driven: un-latch so the next
|
|
1159
|
+
// measure re-derives (and re-latches explicit if the container
|
|
1160
|
+
// now reports a height that isn't ours).
|
|
1161
|
+
isAutoHeight = null;
|
|
1162
|
+
}
|
|
1163
|
+
resize();
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
// Explicit-height (and not-yet-latched) mounts keep today's
|
|
1167
|
+
// re-render-on-every-fire behavior.
|
|
1054
1168
|
resize();
|
|
1055
1169
|
});
|
|
1056
1170
|
}
|