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(-)