This is an automated email from the ASF dual-hosted git repository.

sfirke pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new f69023a9c6c fix(dashboard): freeze nested tabs to the top when 
scrolling down (#43940)
f69023a9c6c is described below

commit f69023a9c6c1b336215a942b563781216ec73008
Author: Sam Firke <[email protected]>
AuthorDate: Wed Sep 9 15:56:23 2026 -0400

    fix(dashboard): freeze nested tabs to the top when scrolling down (#43940)
---
 .../DashboardBuilder/DashboardBuilder.test.tsx     |  98 ++++++++++-
 .../DashboardBuilder/DashboardBuilder.tsx          |  19 ++-
 .../{index.ts => StickyTabsOffsetContext.ts}       |  20 ++-
 .../TabsRenderer/TabsRenderer.test.tsx             | 171 +++++++++++++++++++-
 .../gridComponents/TabsRenderer/TabsRenderer.tsx   | 179 +++++++++++++++------
 .../gridComponents/TabsRenderer/index.ts           |   1 +
 6 files changed, 434 insertions(+), 54 deletions(-)

diff --git 
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx
 
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx
index d2a196845e7..0153d1ab34a 100644
--- 
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx
+++ 
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx
@@ -115,8 +115,21 @@ 
jest.mock('src/dashboard/components/nativeFilters/FilterBar', () => {
   MockFilterBar.displayName = 'MockFilterBar';
   return MockFilterBar;
 });
+// Exposes the sticky offset the builder hands to tab bars in the grid.
 jest.mock('src/dashboard/containers/DashboardGrid', () => {
-  const MockDashboardGrid = () => <div data-test="mock-dashboard-grid" />;
+  const { useContext } = jest.requireActual('react');
+  const { StickyTabsOffsetContext } = jest.requireActual(
+    
'src/dashboard/components/gridComponents/TabsRenderer/StickyTabsOffsetContext',
+  );
+  const MockDashboardGrid = () => {
+    const stickyTabsOffset = useContext(StickyTabsOffsetContext);
+    return (
+      <div
+        data-test="mock-dashboard-grid"
+        data-sticky-tabs-offset={stickyTabsOffset ?? 'none'}
+      />
+    );
+  };
   MockDashboardGrid.displayName = 'MockDashboardGrid';
   return MockDashboardGrid;
 });
@@ -179,6 +192,89 @@ describe('DashboardBuilder', () => {
     expect(stickyContainer).toHaveClass('dashboard');
   });
 
+  // jsdom lays nothing out; report a height for the sticky header so the
+  // offset handed to the grid is distinguishable from "no offset".
+  function mockHeaderHeight(height: number) {
+    return jest
+      .spyOn(HTMLElement.prototype, 'getBoundingClientRect')
+      .mockImplementation(function measure(this: HTMLElement) {
+        const size =
+          this.dataset.test === 'dashboard-header-wrapper' ? height : 0;
+        return {
+          x: 0,
+          y: 0,
+          top: 0,
+          left: 0,
+          right: 0,
+          bottom: size,
+          width: 0,
+          height: size,
+          toJSON: () => ({}),
+        } as DOMRect;
+      });
+  }
+
+  test('hands the sticky header height to tab bars in the grid while viewing', 
async () => {
+    const rectSpy = mockHeaderHeight(120);
+    try {
+      const { findByTestId } = setup();
+      expect(await findByTestId('mock-dashboard-grid')).toHaveAttribute(
+        'data-sticky-tabs-offset',
+        '120',
+      );
+    } finally {
+      rectSpy.mockRestore();
+    }
+  });
+
+  test('leaves tab bars in the grid unpinned in report mode (?standalone=3)', 
async () => {
+    // Report screenshots of large dashboards are captured tile by tile while
+    // scrolling the page; a pinned bar would repeat in every tile.
+    const originalHref = window.location.href;
+    window.history.replaceState({}, '', '/?standalone=3');
+    const rectSpy = mockHeaderHeight(120);
+    try {
+      const { findByTestId } = setup();
+      expect(await findByTestId('mock-dashboard-grid')).toHaveAttribute(
+        'data-sticky-tabs-offset',
+        'none',
+      );
+    } finally {
+      rectSpy.mockRestore();
+      window.history.replaceState({}, '', originalHref);
+    }
+  });
+
+  test('leaves tab bars in the grid unpinned while a chart is maximized', 
async () => {
+    const rectSpy = mockHeaderHeight(120);
+    try {
+      const { findByTestId } = setup({
+        dashboardState: { ...mockState.dashboardState, fullSizeChartId: 123 },
+      });
+      expect(await findByTestId('mock-dashboard-grid')).toHaveAttribute(
+        'data-sticky-tabs-offset',
+        'none',
+      );
+    } finally {
+      rectSpy.mockRestore();
+    }
+  });
+
+  test('leaves tab bars in the grid unpinned in the mobile viewport', async () 
=> {
+    (useIsMobile as jest.Mock).mockReturnValue(true);
+    const rectSpy = mockHeaderHeight(120);
+    try {
+      const { findByTestId } = setup();
+      expect(await findByTestId('mock-dashboard-grid')).toHaveAttribute(
+        'data-sticky-tabs-offset',
+        'none',
+      );
+    } finally {
+      rectSpy.mockRestore();
+      (useIsMobile as jest.Mock).mockReturnValue(false);
+    }
+  });
+
   test('should add the "dashboard--editing" class if editMode=true', () => {
     const { getByTestId } = setup({
       dashboardState: { ...mockState.dashboardState, editMode: true },
diff --git 
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
 
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
index 62e807fb35a..98189ddb497 100644
--- 
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
+++ 
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
@@ -87,6 +87,7 @@ import {
 } from 'src/dashboard/constants';
 import { selectCanRestoreDashboard } from 
'src/features/versionHistory/canRestoreDashboard';
 import { selectIsDashboardVersionPreviewActive } from 
'src/features/versionHistory/reducer';
+import { StickyTabsOffsetContext } from 
'src/dashboard/components/gridComponents/TabsRenderer';
 import { getRootLevelTabsComponent, shouldFocusTabs } from './utils';
 import DashboardContainer from './DashboardContainer';
 import { useNativeFilters } from './state';
@@ -490,8 +491,9 @@ const DashboardBuilder = () => {
   // always get the desktop layout -- matching the pre-existing behavior the
   // docs already promise for embedded dashboards.
   const standaloneMode = getUrlParam(URL_PARAMS.standalone);
+  const isMobileViewport = useIsMobile();
   const isNotMobile =
-    !useIsMobile() || standaloneMode !== DashboardStandaloneMode.None;
+    !isMobileViewport || standaloneMode !== DashboardStandaloneMode.None;
   const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
 
   // Reset the drawer's open state when leaving mobile mode so it doesn't
@@ -774,6 +776,17 @@ const DashboardBuilder = () => {
     ? theme.sizeUnit * 4
     : theme.sizeUnit * 8;
 
+  // Tab bars nested in the grid pin just below the sticky header while the
+  // page scrolls. Not in the mobile viewport, where the header scrolls away
+  // and the mobile styling pins tab bars on its own; not in report mode,
+  // whose tiled screenshots scroll the page and would capture a pinned bar
+  // in every tile; and not while a chart is maximized, which sits inside its
+  // own stacking context and must not be covered by a pinned bar.
+  // (TabsRenderer itself opts out while editing, since drop targets rely on
+  // document flow.)
+  const stickyTabsOffset =
+    isMobileViewport || isReport || fullSizeChartId ? undefined : barTopOffset;
+
   const renderChild = useCallback(
     (adjustedWidth: number) => {
       const filterBarWidth = dashboardFiltersOpen
@@ -976,7 +989,9 @@ const DashboardBuilder = () => {
                   />
                 </div>
               ) : (
-                <DashboardContainer topLevelTabs={topLevelTabs} />
+                <StickyTabsOffsetContext.Provider value={stickyTabsOffset}>
+                  <DashboardContainer topLevelTabs={topLevelTabs} />
+                </StickyTabsOffsetContext.Provider>
               )
             ) : (
               <Loading />
diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/index.ts
 
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/StickyTabsOffsetContext.ts
similarity index 50%
copy from 
superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/index.ts
copy to 
superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/StickyTabsOffsetContext.ts
index 320e8ebb022..60262f10f9b 100644
--- 
a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/index.ts
+++ 
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/StickyTabsOffsetContext.ts
@@ -16,5 +16,21 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-export { default } from './TabsRenderer';
-export type { TabsRendererProps, TabItem, TabsComponent } from 
'./TabsRenderer';
+import { createContext } from 'react';
+
+/**
+ * Distance, in pixels, from the top of the viewport at which a tab bar
+ * rendered inside the dashboard grid should stick while the page scrolls.
+ *
+ * The dashboard header (title plus top-level tabs) is itself sticky, so the
+ * first level of nested tabs pins just below it. Each level of nested tabs
+ * then adds its own tab bar height for the tabs it contains, so deeper tab
+ * bars stack beneath the ones above them instead of overlapping.
+ *
+ * `undefined` disables sticky tab bars, which is the case while editing
+ * (drag-and-drop targets are laid out in document flow) and in the mobile
+ * consumption experience, which pins tab bars through its own styling.
+ */
+export const StickyTabsOffsetContext = createContext<number | undefined>(
+  undefined,
+);
diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.test.tsx
 
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.test.tsx
index 3f914f5546f..c74dc34ee47 100644
--- 
a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.test.tsx
+++ 
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.test.tsx
@@ -16,8 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { fireEvent, render, screen } from 'spec/helpers/testing-library';
+import {
+  act,
+  fireEvent,
+  render,
+  screen,
+  sleep,
+} from 'spec/helpers/testing-library';
 import TabsRenderer, { TabItem, TabsRendererProps } from './TabsRenderer';
+import { StickyTabsOffsetContext } from './StickyTabsOffsetContext';
 
 const mockTabItems: TabItem[] = [
   {
@@ -240,7 +247,7 @@ describe('TabsRenderer', () => {
     expect(screen.queryByText('Tab 2 Content')).not.toBeInTheDocument(); // 
Not active
   });
 
-  test('drags from the tab title and shows the drag indicator only then', () 
=> {
+  test('drags from the tab title and shows the drag indicator only then', 
async () => {
     render(<TabsRenderer {...draggableTabProps} />);
     const container = screen.getByTestId('dashboard-component-tabs');
     const title = container.querySelector('textarea') as HTMLTextAreaElement;
@@ -263,5 +270,165 @@ describe('TabsRenderer', () => {
     expect(container).toHaveStyleRule('cursor', 'move', {
       target: '.dragdroppable-tab *',
     });
+
+    // Release the pointer so the drag does not outlive this test. dnd-kit
+    // keeps swallowing clicks on the shared document for 50ms after a drag
+    // ends, which would eat the tab click of whichever test runs next.
+    fireEvent.pointerUp(document, { button: 0, isPrimary: true, clientX: 50 });
+    await sleep(60);
+  });
+
+  // jsdom's cascade ignores specificity, so assert on the emotion rule rather
+  // than the computed style, which antd's own `position: relative` would win
+  const TAB_BAR = { target: /> ?\.ant-tabs ?> ?\.ant-tabs-nav$/ };
+
+  test('pins the tab bar below the offset supplied by the dashboard', () => {
+    render(
+      <StickyTabsOffsetContext.Provider value={64}>
+        <TabsRenderer {...mockProps} />
+      </StickyTabsOffsetContext.Provider>,
+    );
+    const container = screen.getByTestId('dashboard-component-tabs');
+
+    expect(container).toHaveStyleRule('position', 'sticky', TAB_BAR);
+    expect(container).toHaveStyleRule('top', '64px', TAB_BAR);
+  });
+
+  test('leaves the tab bar in document flow without a dashboard offset', () => 
{
+    render(<TabsRenderer {...mockProps} />);
+    const container = screen.getByTestId('dashboard-component-tabs');
+
+    expect(container).not.toHaveStyleRule('position', 'sticky', TAB_BAR);
+  });
+
+  test('leaves the tab bar in document flow in edit mode', () => {
+    render(
+      <StickyTabsOffsetContext.Provider value={64}>
+        <TabsRenderer {...mockProps} editMode />
+      </StickyTabsOffsetContext.Provider>,
+    );
+    const container = screen.getByTestId('dashboard-component-tabs');
+
+    expect(container).not.toHaveStyleRule('position', 'sticky', TAB_BAR);
+  });
+
+  // Reports the offset this tab set hands to tab sets nested inside it.
+  const nestedTabItems: TabItem[] = [
+    {
+      ...mockTabItems[0],
+      children: (
+        <StickyTabsOffsetContext.Consumer>
+          {offset => <div data-test="nested-offset">{offset}</div>}
+        </StickyTabsOffsetContext.Consumer>
+      ),
+    },
+    mockTabItems[1],
+  ];
+
+  test('stacks nested tab bars beneath its own tab bar', () => {
+    // jsdom lays nothing out, so give the tab bar a height to add up
+    const heightSpy = jest
+      .spyOn(HTMLElement.prototype, 'offsetHeight', 'get')
+      .mockReturnValue(40);
+
+    try {
+      render(
+        <StickyTabsOffsetContext.Provider value={64}>
+          <TabsRenderer {...mockProps} tabItems={nestedTabItems} />
+        </StickyTabsOffsetContext.Provider>,
+      );
+
+      expect(screen.getByTestId('nested-offset')).toHaveTextContent('104');
+    } finally {
+      heightSpy.mockRestore();
+    }
+  });
+
+  test('restacks nested tab bars when its own tab bar reflows', () => {
+    // The shared jsdom shim never fires its callback, so stand in an observer
+    // that hands the callback back to the test. The component re-measures the
+    // element rather than reading the entries, so the height comes from the
+    // spy below; the callback only needs to run.
+    const observerCallbacks: ResizeObserverCallback[] = [];
+    const RealResizeObserver = window.ResizeObserver;
+    window.ResizeObserver = class {
+      constructor(callback: ResizeObserverCallback) {
+        observerCallbacks.push(callback);
+      }
+
+      observe() {}
+
+      unobserve() {}
+
+      disconnect() {}
+    } as unknown as typeof ResizeObserver;
+    const heightSpy = jest
+      .spyOn(HTMLElement.prototype, 'offsetHeight', 'get')
+      .mockReturnValue(40);
+
+    try {
+      render(
+        <StickyTabsOffsetContext.Provider value={64}>
+          <TabsRenderer {...mockProps} tabItems={nestedTabItems} />
+        </StickyTabsOffsetContext.Provider>,
+      );
+      expect(screen.getByTestId('nested-offset')).toHaveTextContent('104');
+
+      // The bar grows -- labels wrap on a narrow viewport, a webfont lands --
+      // and the tab set below it has to move down by the same amount.
+      heightSpy.mockReturnValue(80);
+      act(() => {
+        observerCallbacks.forEach(callback =>
+          callback([], {} as ResizeObserver),
+        );
+      });
+
+      expect(screen.getByTestId('nested-offset')).toHaveTextContent('144');
+    } finally {
+      heightSpy.mockRestore();
+      window.ResizeObserver = RealResizeObserver;
+    }
+  });
+
+  // Switching tabs while the bar is pinned: the page scrolls so the tab set
+  // starts where its bar is pinned, mirroring the top-level tabs' jump to top
+  function renderPinnedTabSet(containerTop: number, offset?: number) {
+    const scrollTo = jest.spyOn(window, 'scrollTo').mockImplementation();
+    const rectSpy = jest
+      .spyOn(HTMLElement.prototype, 'getBoundingClientRect')
+      .mockReturnValue({ top: containerTop } as DOMRect);
+    const scrollYStub = jest.replaceProperty(window, 'scrollY', 500);
+    render(
+      <StickyTabsOffsetContext.Provider value={offset}>
+        <TabsRenderer {...mockProps} />
+      </StickyTabsOffsetContext.Provider>,
+    );
+    fireEvent.click(screen.getByText('Tab 2').closest('[role="tab"]')!);
+    rectSpy.mockRestore();
+    scrollYStub.restore();
+    return scrollTo;
+  }
+
+  test('scrolls a pinned tab set back to its top when switching tabs', () => {
+    // the tab set's top is 200px above the viewport, so the bar is pinned
+    const scrollTo = renderPinnedTabSet(-200, 64);
+
+    expect(scrollTo).toHaveBeenCalledWith(window.scrollX, 500 - 200 - 64);
+    scrollTo.mockRestore();
+  });
+
+  test('leaves the page alone when the tab bar is not pinned', () => {
+    // the tab set starts below where its bar would pin, so it is in flow
+    const scrollTo = renderPinnedTabSet(300, 64);
+
+    expect(scrollTo).not.toHaveBeenCalled();
+    scrollTo.mockRestore();
+  });
+
+  test('leaves the page alone without a dashboard offset', () => {
+    const scrollTo = renderPinnedTabSet(-200);
+
+    expect(scrollTo).not.toHaveBeenCalled();
+    scrollTo.mockRestore();
   });
 });
diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
 
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
index d5a4045f9a8..1d2797fb192 100644
--- 
a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
+++ 
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
@@ -22,6 +22,8 @@ import {
   ReactElement,
   RefObject,
   useCallback,
+  useContext,
+  useEffect,
   useRef,
   useState,
 } from 'react';
@@ -46,11 +48,33 @@ import {
 import HoverMenu from '../../menu/HoverMenu';
 import DragHandle from '../../dnd/DragHandle';
 import DeleteComponentButton from '../../DeleteComponentButton';
+import { StickyTabsOffsetContext } from './StickyTabsOffsetContext';
 
-const StyledTabsContainer = styled.div<{ isDragging?: boolean }>`
+// @z-index-above-dashboard-charts: above chart content, below the sticky
+// dashboard header (99) and the filter bar (11)
+const STICKY_TAB_BAR_Z_INDEX = 10;
+
+const StyledTabsContainer = styled.div<{
+  isDragging?: boolean;
+  stickyTop?: number;
+}>`
   width: 100%;
   background-color: ${({ theme }) => theme.colorBgContainer};
 
+  /* Pin this component's own tab bar (direct child only, so nested tab
+     sets keep their own offsets) below the sticky dashboard header while
+     its content scrolls. */
+  ${({ theme, stickyTop }) =>
+    stickyTop !== undefined &&
+    css`
+      & > .ant-tabs > .ant-tabs-nav {
+        position: sticky;
+        top: ${stickyTop}px;
+        z-index: ${STICKY_TAB_BAR_Z_INDEX};
+        background-color: ${theme.colorBgContainer};
+      }
+    `}
+
   & .dashboard-component-tabs-content {
     height: 100%;
   }
@@ -231,6 +255,59 @@ const TabsRenderer = memo<TabsRendererProps>(
   }) => {
     const [activeId, setActiveId] = useState<string | null>(null);
 
+    // Sticky tab bars only apply in view mode: while editing, drag-and-drop
+    // targets and hover menus are positioned against the tab bar's place in
+    // document flow.
+    const parentStickyOffset = useContext(StickyTabsOffsetContext);
+    const stickyTop = editMode ? undefined : parentStickyOffset;
+    const containerRef = useRef<HTMLDivElement>(null);
+    const [tabBarHeight, setTabBarHeight] = useState(0);
+
+    // Keyed on whether the bar is pinned rather than on the offset itself:
+    // the header re-measuring on every resize would otherwise tear down and
+    // recreate the observer in every tab set on the page.
+    const isPinned = stickyTop !== undefined;
+    useEffect(() => {
+      // Direct-child selector, so nested tab sets' bars are never matched.
+      const tabBar = isPinned
+        ? containerRef.current?.querySelector<HTMLElement>(
+            ':scope > .ant-tabs > .ant-tabs-nav',
+          )
+        : null;
+      if (!tabBar) {
+        return undefined;
+      }
+      const measure = () => setTabBarHeight(tabBar.offsetHeight);
+      measure();
+      // Matches the sticky header's own guard in DashboardBuilder, for
+      // environments without ResizeObserver: the bar still pins, it just
+      // keeps the height measured at mount.
+      if (!global.hasOwnProperty('ResizeObserver')) {
+        return undefined;
+      }
+      const observer = new ResizeObserver(measure);
+      observer.observe(tabBar);
+      return () => observer.disconnect();
+    }, [isPinned]);
+
+    // Tabs nested inside this one stack their bar beneath ours.
+    const childStickyOffset =
+      stickyTop === undefined ? undefined : stickyTop + tabBarHeight;
+
+    // Counterpart of the top-level tabs, which scroll the page to the top on
+    // every switch (DashboardBuilder.handleChangeTab). With this bar pinned,
+    // a switch would otherwise land the reader partway down the new tab's
+    // content; bring the tab set back to where its bar is pinned instead.
+    const scrollPinnedTabSetToTop = () => {
+      if (stickyTop === undefined || !containerRef.current) {
+        return;
+      }
+      const { top } = containerRef.current.getBoundingClientRect();
+      if (top < stickyTop) {
+        window.scrollTo(window.scrollX, window.scrollY + top - stickyTop);
+      }
+    };
+
     // Use ref to always have access to the current tabIds in callbacks
     const tabIdsRef = useRef(tabIds);
     tabIdsRef.current = tabIds;
@@ -267,9 +344,11 @@ const TabsRenderer = memo<TabsRendererProps>(
 
     return (
       <StyledTabsContainer
+        ref={containerRef}
         className="dashboard-component dashboard-component-tabs"
         data-test="dashboard-component-tabs"
         isDragging={isDragging}
+        stickyTop={stickyTop}
       >
         {editMode && renderHoverMenu && tabsDragSourceRef && (
           <HoverMenu innerRef={tabsDragSourceRef} position="left">
@@ -278,53 +357,59 @@ const TabsRenderer = memo<TabsRendererProps>(
           </HoverMenu>
         )}
 
-        <LineEditableTabs
-          id={tabsComponent.id}
-          activeKey={activeKey}
-          onChange={key => {
-            if (typeof key === 'string') {
-              const tabIndex = tabIds.indexOf(key);
-              if (tabIndex !== -1) handleClickTab(tabIndex);
-            }
-          }}
-          onEdit={handleEdit}
-          data-test="nav-list"
-          type={editMode ? 'editable-card' : 'card'}
-          items={tabItems}
-          tabBarStyle={{ paddingLeft: tabBarPaddingLeft }}
-          fullHeight
-          {...(editMode && {
-            renderTabBar: (tabBarProps, DefaultTabBar) => (
-              <DndContext
-                key={tabIds.join('-')}
-                sensors={[sensor]}
-                onDragStart={onDragStart}
-                onDragEnd={onDragEnd}
-                onDragCancel={onDragCancel}
-                collisionDetection={closestCenter}
-              >
-                <SortableContext
-                  items={tabIds}
-                  strategy={horizontalListSortingStrategy}
+        <StickyTabsOffsetContext.Provider value={childStickyOffset}>
+          <LineEditableTabs
+            id={tabsComponent.id}
+            activeKey={activeKey}
+            onChange={key => {
+              if (typeof key === 'string') {
+                const tabIndex = tabIds.indexOf(key);
+                if (tabIndex !== -1) {
+                  handleClickTab(tabIndex);
+                  scrollPinnedTabSetToTop();
+                }
+              }
+            }}
+            onEdit={handleEdit}
+            data-test="nav-list"
+            type={editMode ? 'editable-card' : 'card'}
+            items={tabItems}
+            tabBarStyle={{ paddingLeft: tabBarPaddingLeft }}
+            fullHeight
+            {...(editMode && {
+              renderTabBar: (tabBarProps, DefaultTabBar) => (
+                <DndContext
+                  key={tabIds.join('-')}
+                  sensors={[sensor]}
+                  onDragStart={onDragStart}
+                  onDragEnd={onDragEnd}
+                  onDragCancel={onDragCancel}
+                  collisionDetection={closestCenter}
                 >
-                  <DefaultTabBar {...tabBarProps}>
-                    {(node: React.ReactElement) => (
-                      <DraggableTabNode
-                        {...(node as React.ReactElement<DraggableTabNodeProps>)
-                          .props}
-                        key={node.key}
-                        data-node-key={node.key as string}
-                        disabled={isEditingTabTitle}
-                      >
-                        {node}
-                      </DraggableTabNode>
-                    )}
-                  </DefaultTabBar>
-                </SortableContext>
-              </DndContext>
-            ),
-          })}
-        />
+                  <SortableContext
+                    items={tabIds}
+                    strategy={horizontalListSortingStrategy}
+                  >
+                    <DefaultTabBar {...tabBarProps}>
+                      {(node: React.ReactElement) => (
+                        <DraggableTabNode
+                          {...(
+                            node as React.ReactElement<DraggableTabNodeProps>
+                          ).props}
+                          key={node.key}
+                          data-node-key={node.key as string}
+                          disabled={isEditingTabTitle}
+                        >
+                          {node}
+                        </DraggableTabNode>
+                      )}
+                    </DefaultTabBar>
+                  </SortableContext>
+                </DndContext>
+              ),
+            })}
+          />
+        </StickyTabsOffsetContext.Provider>
       </StyledTabsContainer>
     );
   },
diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/index.ts
 
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/index.ts
index 320e8ebb022..9c4e0257d3c 100644
--- 
a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/index.ts
+++ 
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/index.ts
@@ -18,3 +18,4 @@
  */
 export { default } from './TabsRenderer';
 export type { TabsRendererProps, TabItem, TabsComponent } from 
'./TabsRenderer';
+export { StickyTabsOffsetContext } from './StickyTabsOffsetContext';

Reply via email to