You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jl...@apache.org on 2026/09/18 18:05:12 UTC
(superset) branch ultraset-fix-44415 created (now 14fb050d643)
This is an automated email from the ASF dual-hosted git repository.
sadpandajoe pushed a change to branch ultraset-fix-44415
in repository https://gitbox.apache.org/repos/asf/superset.git
at 14fb050d643 Merge remote-tracking branch 'origin/master' into ultraset-fix-44415
This branch includes the following new commits:
new 9c35bfdfacd fix(plugin-chart-table): re-measure sticky layout when the table gains a box
new 14fb050d643 Merge remote-tracking branch 'origin/master' into ultraset-fix-44415
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(superset) 01/02: fix(plugin-chart-table): re-measure sticky layout when the table gains a box
Posted by jl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sadpandajoe pushed a commit to branch ultraset-fix-44415
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 9c35bfdfacda8490915d4e2e64cdf3c6b3c94ec5
Author: Joe Li <jo...@preset.io>
AuthorDate: Fri Sep 18 18:03:23 2026 +0000
fix(plugin-chart-table): re-measure sticky layout when the table gains a box
A table chart that mounts inside a `display: none` subtree -- an inactive
dashboard tab, for instance -- measures its header against zero-sized boxes,
so `StickyWrap`'s layout effect bails out at its `if (!theadHeight) return;`
guard, computes no sticky layout, and renders only its `visibility: hidden`
sizer. The chart is blank even though its query response arrived fine.
None of that effect's dependencies (`maxWidth`, `maxHeight`,
`setStickyState`, the scrollbar size) change when the tab becomes visible --
the width and height are static grid-layout math, not measurements -- so the
measurement never runs again and the chart stays blank until it is
force-refreshed or the window is resized.
Observe the wrapper with a ResizeObserver while no sticky layout has been
computed, and measure again once it gains a box.
Fixes #44415
Co-Authored-By: Claude Opus 5 <no...@anthropic.com>
---
.../src/DataTable/hooks/useSticky.tsx | 30 +++++++-
.../test/DataTable/hooks/useSticky.test.tsx | 85 +++++++++++++++++++++-
2 files changed, 111 insertions(+), 4 deletions(-)
diff --git a/superset-frontend/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx b/superset-frontend/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx
index eee392e0c0e..30df5de6d8c 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx
@@ -23,6 +23,7 @@ import {
cloneElement,
useRef,
useMemo,
+ useEffect,
useLayoutEffect,
useCallback,
ReactNode,
@@ -169,6 +170,7 @@ function StickyWrap({
const scrollHeaderRef = useRef<HTMLDivElement>(null); // fixed header
const scrollFooterRef = useRef<HTMLDivElement>(null); // fixed footer
const scrollBodyRef = useRef<HTMLDivElement>(null); // main body
+ const wrapRef = useRef<HTMLDivElement>(null); // outer container, observed for resizes
const scrollBarSize = getCustomScrollBarSize();
const { bodyHeight, columnWidths, hasVerticalScroll } = sticky;
@@ -178,8 +180,7 @@ function StickyWrap({
sticky.height !== maxHeight ||
sticky.setStickyState !== setStickyState;
- // update scrollable area and header column sizes when mounted
- useLayoutEffect(() => {
+ const measure = useCallback(() => {
if (!theadRef.current) {
return;
}
@@ -224,6 +225,30 @@ function StickyWrap({
});
}, [maxWidth, maxHeight, setStickyState, scrollBarSize]);
+ // update scrollable area and header column sizes when mounted
+ useLayoutEffect(() => {
+ measure();
+ }, [measure]);
+
+ // A `display: none` ancestor -- an inactive dashboard tab, for instance --
+ // gives the table no box, so the measurement above bails out and no sticky
+ // layout is computed, leaving the chart blank. None of that measurement's
+ // dependencies change when the table later gains a box, so watch for it
+ // directly and measure again. `measure` re-reads the DOM itself, so a
+ // still-boxless notification is a no-op, and the observer is only attached
+ // while there is no layout to show.
+ useEffect(() => {
+ const wrap = wrapRef.current;
+ if (columnWidths || !wrap || typeof ResizeObserver === 'undefined') {
+ return undefined;
+ }
+ const observer = new ResizeObserver(() => {
+ measure();
+ });
+ observer.observe(wrap);
+ return () => observer.disconnect();
+ }, [columnWidths, measure]);
+
let sizerTable: ReactElement | undefined;
let headerTable: ReactElement | undefined;
let footerTable: ReactElement | undefined;
@@ -395,6 +420,7 @@ function StickyWrap({
// positioned independently; a real <table> would break that layout, so
// role="table" is the correct ARIA pattern here, not the suggested tag.
<div
+ ref={wrapRef}
style={{
width: maxWidth,
height: sticky.realHeight || maxHeight,
diff --git a/superset-frontend/plugins/plugin-chart-table/test/DataTable/hooks/useSticky.test.tsx b/superset-frontend/plugins/plugin-chart-table/test/DataTable/hooks/useSticky.test.tsx
index d75eca12fc4..4f760dd0b6b 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/DataTable/hooks/useSticky.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/test/DataTable/hooks/useSticky.test.tsx
@@ -18,7 +18,7 @@
*/
import { useCallback } from 'react';
import { useTable, Column } from 'react-table';
-import { render } from '@superset-ui/core/spec';
+import { act, render } from '@superset-ui/core/spec';
import useSticky from '../../../src/DataTable/hooks/useSticky';
// A value distinguishable from any real scrollbar width, so the width
@@ -47,10 +47,21 @@ const TOTAL_FOOTER_HEIGHT = 30;
// sticky layout effect computes `hasVerticalScroll: true`.
const FULL_TABLE_HEIGHT = 400;
+// An element inside a `display: none` subtree (an inactive dashboard tab,
+// for instance) generates no box at all, so every measurement of it reads
+// zero. jsdom does no layout, so the fake has to model that itself.
+function isInHiddenSubtree(el: HTMLElement) {
+ for (let node: HTMLElement | null = el; node; node = node.parentElement) {
+ if (node.style?.display === 'none') return true;
+ }
+ return false;
+}
+
function mockMeasurements() {
jest
.spyOn(HTMLElement.prototype, 'clientHeight', 'get')
.mockImplementation(function mockClientHeight(this: HTMLElement) {
+ if (isInHiddenSubtree(this)) return 0;
if (this.tagName === 'THEAD') return TOTAL_HEADER_HEIGHT;
if (this.tagName === 'TFOOT') return TOTAL_FOOTER_HEIGHT;
if (this.tagName === 'TABLE') return FULL_TABLE_HEIGHT;
@@ -59,7 +70,7 @@ function mockMeasurements() {
jest
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
.mockImplementation(function mockRect(this: HTMLElement) {
- const width = this.tagName === 'TH' ? 60 : 0;
+ const width = this.tagName === 'TH' && !isInHiddenSubtree(this) ? 60 : 0;
return {
width,
height: 0,
@@ -203,3 +214,73 @@ test('sticky header/footer width matches the body, independent of the scrollbar-
jest.restoreAllMocks();
});
+
+function installResizeObserverMock() {
+ const callbacks: ResizeObserverCallback[] = [];
+ const original = globalThis.ResizeObserver;
+ globalThis.ResizeObserver = class {
+ constructor(callback: ResizeObserverCallback) {
+ callbacks.push(callback);
+ }
+
+ observe() {}
+
+ unobserve() {}
+
+ disconnect() {}
+ } as unknown as typeof ResizeObserver;
+ return {
+ trigger: () =>
+ callbacks.forEach(callback => callback([], {} as ResizeObserver)),
+ restore: () => {
+ globalThis.ResizeObserver = original;
+ },
+ };
+}
+
+// When useSticky cannot measure, `StickyWrap` renders only its hidden
+// "sizer" table (`visibility: hidden`), which still contains a full tbody.
+// So count only the data cells a user could actually see.
+function visibleDataCellCount(container: HTMLElement) {
+ return Array.from(container.querySelectorAll('tbody td')).filter(cell => {
+ for (
+ let node: HTMLElement | null = cell as HTMLElement;
+ node;
+ node = node.parentElement
+ ) {
+ if (node.style?.visibility === 'hidden') return false;
+ }
+ return true;
+ }).length;
+}
+
+test('sticky table paints once it gains a box, when first rendered inside a hidden dashboard tab', () => {
+ mockMeasurements();
+ const resizeObserver = installResizeObserverMock();
+
+ // An inactive dashboard tab keeps its charts mounted under `display: none`,
+ // so a chart that (re)mounts while its tab is hidden does its one and only
+ // layout measurement against zero-sized boxes.
+ const host = document.createElement('div');
+ host.style.display = 'none';
+ document.body.append(host);
+
+ render(<StickyTableHarness />, { container: host });
+
+ expect(visibleDataCellCount(host)).toBe(0);
+
+ // Switching to the tab gives the chart a real box. Neither `maxWidth`,
+ // `maxHeight`, `setStickyState` nor the scrollbar size changes, so without
+ // the ResizeObserver the sticky layout is never recomputed and the chart
+ // stays blank until it is force-refreshed or the window is resized.
+ host.style.display = '';
+ act(() => {
+ resizeObserver.trigger();
+ });
+
+ expect(visibleDataCellCount(host)).toBe(data.length * columns.length);
+
+ host.remove();
+ resizeObserver.restore();
+ jest.restoreAllMocks();
+});
(superset) 02/02: Merge remote-tracking branch 'origin/master' into ultraset-fix-44415
Posted by jl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sadpandajoe pushed a commit to branch ultraset-fix-44415
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 14fb050d643f9d0d1f68cc8c6bb5e72397c5117a
Merge: 9c35bfdfacd 16090803bbc
Author: Joe Li <jo...@preset.io>
AuthorDate: Fri Sep 18 18:03:29 2026 +0000
Merge remote-tracking branch 'origin/master' into ultraset-fix-44415
superset-frontend/package-lock.json | 97 +++++++---------------
superset-frontend/package.json | 6 +-
.../plugins/plugin-chart-echarts/package.json | 2 +-
3 files changed, 33 insertions(+), 72 deletions(-)