You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/08/08 06:02:33 UTC

(superset) branch master updated: chore(deps): bump react-window and @types/react-window in /superset-frontend (#42528)

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

rusackas 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 38727906a2b chore(deps): bump react-window and @types/react-window in /superset-frontend (#42528)
38727906a2b is described below

commit 38727906a2b1c68f3c4c56409ea6e94de01fb48a
Author: dependabot[bot] <49...@users.noreply.github.com>
AuthorDate: Fri Aug 7 23:02:27 2026 -0700

    chore(deps): bump react-window and @types/react-window in /superset-frontend (#42528)
    
    Signed-off-by: dependabot[bot] <su...@github.com>
    Co-authored-by: dependabot[bot] <49...@users.noreply.github.com>
    Co-authored-by: Amin Ghadersohi <am...@gmail.com>
    Co-authored-by: Claude Sonnet 5 <no...@anthropic.com>
---
 superset-frontend/package-lock.json                |  69 ++++-----
 superset-frontend/package.json                     |   3 +-
 .../packages/superset-core/package.json            |   1 -
 .../packages/superset-ui-core/package.json         |   1 -
 .../src/components/Table/VirtualTable.test.tsx     | 122 ++++++++++++++++
 .../src/components/Table/VirtualTable.tsx          | 155 ++++++++++++---------
 .../components/Chart/DrillBy/DrillBySubmenu.tsx    |  68 +++++----
 .../FoldersEditor/FoldersEditor.test.tsx           |  26 ++--
 .../FoldersEditor/VirtualizedTreeItem.tsx          |  54 +++----
 .../FoldersEditor/VirtualizedTreeList.tsx          |  47 +++----
 .../FoldersEditor/hooks/useAutoScroll.ts           |   8 +-
 .../src/dashboard/components/SliceAdder.test.tsx   |  56 +++++++-
 .../src/dashboard/components/SliceAdder.tsx        | 131 +++++++++--------
 .../components/DatasourcePanel/DatasourceItems.tsx |  32 ++---
 .../DatasourcePanel/DatasourcePanelItem.test.tsx   |  19 ++-
 .../DatasourcePanel/DatasourcePanelItem.tsx        |  35 +++--
 16 files changed, 516 insertions(+), 311 deletions(-)

diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index ac43a67846e..4d53e7ae5f0 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -139,7 +139,7 @@
         "react-table": "^7.8.0",
         "react-transition-group": "^4.4.5",
         "react-virtualized-auto-sizer": "^1.0.26",
-        "react-window": "^1.8.10",
+        "react-window": "^2.3.0",
         "redux": "^4.2.1",
         "redux-localstorage": "^0.4.1",
         "redux-thunk": "^2.1.0",
@@ -207,7 +207,6 @@
         "@types/react-redux": "^7.1.10",
         "@types/react-router-dom": "^5.3.3",
         "@types/react-transition-group": "^4.4.12",
-        "@types/react-window": "^1.8.8",
         "@types/redux-localstorage": "^1.0.8",
         "@types/redux-mock-store": "^1.0.6",
         "@types/rison": "0.1.0",
@@ -12856,15 +12855,6 @@
         "@types/react": "*"
       }
     },
-    "node_modules/@types/react-window": {
-      "version": "1.8.8",
-      "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz",
-      "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/react": "*"
-      }
-    },
     "node_modules/@types/redux-localstorage": {
       "version": "1.0.12",
       "resolved": "https://registry.npmjs.org/@types/redux-localstorage/-/redux-localstorage-1.0.12.tgz",
@@ -34473,6 +34463,12 @@
         "@babel/runtime": "^7.9.2"
       }
     },
+    "node_modules/react-arborist/node_modules/memoize-one": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
+      "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
+      "license": "MIT"
+    },
     "node_modules/react-arborist/node_modules/react-dnd": {
       "version": "14.0.5",
       "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.5.tgz",
@@ -34512,6 +34508,23 @@
         "dnd-core": "14.0.1"
       }
     },
+    "node_modules/react-arborist/node_modules/react-window": {
+      "version": "1.8.11",
+      "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz",
+      "integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.0.0",
+        "memoize-one": ">=3.1.1 <6"
+      },
+      "engines": {
+        "node": ">8.0.0"
+      },
+      "peerDependencies": {
+        "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
     "node_modules/react-arborist/node_modules/redux": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
@@ -35200,28 +35213,15 @@
       }
     },
     "node_modules/react-window": {
-      "version": "1.8.11",
-      "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz",
-      "integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.3.0.tgz",
+      "integrity": "sha512-FW6TIpaOH646k51X7yE+LSCWGkt5Pfsnc1fVyq/sCI9h0pTqmMiBXM04pzFKg3Bt7NGkeV6kqbU8d/QjmFS7Ug==",
       "license": "MIT",
-      "dependencies": {
-        "@babel/runtime": "^7.0.0",
-        "memoize-one": ">=3.1.1 <6"
-      },
-      "engines": {
-        "node": ">8.0.0"
-      },
       "peerDependencies": {
-        "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
-        "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+        "react": "^18.0.0 || ^19.0.0",
+        "react-dom": "^18.0.0 || ^19.0.0"
       }
     },
-    "node_modules/react-window/node_modules/memoize-one": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
-      "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
-      "license": "MIT"
-    },
     "node_modules/read": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/read/-/read-4.1.0.tgz",
@@ -42790,7 +42790,6 @@
         "@types/lodash": "^4.17.24",
         "@types/react": "*",
         "@types/react-loadable": "*",
-        "@types/react-window": "^1.8.8",
         "@types/tinycolor2": "*",
         "typescript": "^5.0.0"
       },
@@ -42916,7 +42915,6 @@
         "@testing-library/user-event": "*",
         "@types/react": "*",
         "@types/react-loadable": "*",
-        "@types/react-window": "^1.8.8",
         "@types/tinycolor2": "*",
         "antd": "^6.0.0",
         "nanoid": "*",
@@ -42972,15 +42970,6 @@
         "node": ">=12"
       }
     },
-    "packages/superset-ui-core/node_modules/dompurify": {
-      "version": "3.4.13",
-      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
-      "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
-      "license": "(MPL-2.0 OR Apache-2.0)",
-      "optionalDependencies": {
-        "@types/trusted-types": "^2.0.7"
-      }
-    },
     "packages/superset-ui-core/node_modules/react-ace": {
       "version": "14.0.1",
       "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-14.0.1.tgz",
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index 4840c664e26..5da9b916e56 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -216,7 +216,7 @@
     "react-table": "^7.8.0",
     "react-transition-group": "^4.4.5",
     "react-virtualized-auto-sizer": "^1.0.26",
-    "react-window": "^1.8.10",
+    "react-window": "^2.3.0",
     "redux": "^4.2.1",
     "redux-localstorage": "^0.4.1",
     "redux-thunk": "^2.1.0",
@@ -284,7 +284,6 @@
     "@types/react-redux": "^7.1.10",
     "@types/react-router-dom": "^5.3.3",
     "@types/react-transition-group": "^4.4.12",
-    "@types/react-window": "^1.8.8",
     "@types/redux-localstorage": "^1.0.8",
     "@types/redux-mock-store": "^1.0.6",
     "@types/rison": "0.1.0",
diff --git a/superset-frontend/packages/superset-core/package.json b/superset-frontend/packages/superset-core/package.json
index 98270cfacf8..e08bd419a54 100644
--- a/superset-frontend/packages/superset-core/package.json
+++ b/superset-frontend/packages/superset-core/package.json
@@ -109,7 +109,6 @@
     "@types/lodash": "^4.17.24",
     "@types/react": "*",
     "@types/react-loadable": "*",
-    "@types/react-window": "^1.8.8",
     "@types/tinycolor2": "*",
     "typescript": "^5.0.0"
   },
diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json
index 78a335d02b8..1191a74503c 100644
--- a/superset-frontend/packages/superset-ui-core/package.json
+++ b/superset-frontend/packages/superset-ui-core/package.json
@@ -123,7 +123,6 @@
     "@testing-library/user-event": "*",
     "@types/react": "*",
     "@types/react-loadable": "*",
-    "@types/react-window": "^1.8.8",
     "@types/tinycolor2": "*",
     "antd": "^6.0.0",
     "nanoid": "*",
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Table/VirtualTable.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/Table/VirtualTable.test.tsx
new file mode 100644
index 00000000000..f78c55a9381
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/src/components/Table/VirtualTable.test.tsx
@@ -0,0 +1,122 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { render, screen, waitFor } from '@superset-ui/core/spec';
+import type { ColumnsType } from 'antd/es/table';
+import { Table } from './index';
+
+// These tests exercise VirtualTable's react-window v2 `Grid` wiring
+// (`cellComponent`/`cellProps`/`gridRef`), which previously had no direct
+// coverage - `Table.test.tsx` only exercises the non-virtualized code path.
+
+interface BasicData {
+  columnName: string;
+  columnType: string;
+}
+
+const testData: BasicData[] = [
+  { columnName: 'Number', columnType: 'Numerical' },
+  { columnName: 'String', columnType: 'Physical' },
+  { columnName: 'Date', columnType: 'Virtual' },
+];
+
+const testColumns: ColumnsType<BasicData> = [
+  {
+    title: 'Column Name',
+    dataIndex: 'columnName',
+    key: 'columnName',
+    width: 150,
+  },
+  {
+    title: 'Column Type',
+    dataIndex: 'columnType',
+    key: 'columnType',
+    width: 150,
+  },
+];
+
+test('virtualized table renders headers and row content through the react-window Grid', async () => {
+  render(
+    <Table
+      columns={testColumns}
+      data={testData}
+      virtualize
+      height={200}
+      usePagination={false}
+    />,
+  );
+
+  await waitFor(() =>
+    testColumns.forEach(column =>
+      expect(
+        screen
+          .getAllByText(column.title as string)
+          .find(el => el.closest('th')),
+      ).toBeInTheDocument(),
+    ),
+  );
+
+  testData.forEach(row => {
+    expect(screen.getByText(row.columnName)).toBeInTheDocument();
+  });
+});
+
+test('virtualized table cells keep the DOM hooks other code (cypress, downloadAsImage) relies on', async () => {
+  const { container } = render(
+    <Table
+      columns={testColumns}
+      data={testData}
+      virtualize
+      height={200}
+      usePagination={false}
+    />,
+  );
+
+  await waitFor(() => {
+    expect(container.querySelector('.virtual-grid')).toBeInTheDocument();
+    expect(
+      container.querySelectorAll('.virtual-table-cell').length,
+    ).toBeGreaterThan(0);
+  });
+});
+
+test('cell render functions receive their row data via cellProps rather than a stale closure', async () => {
+  const columnsWithRender: ColumnsType<BasicData> = [
+    {
+      title: 'Column Name',
+      dataIndex: 'columnName',
+      key: 'columnName',
+      width: 150,
+      render: (value: string) => `rendered:${value}`,
+    },
+  ];
+
+  render(
+    <Table
+      columns={columnsWithRender}
+      data={testData}
+      virtualize
+      height={200}
+      usePagination={false}
+    />,
+  );
+
+  await waitFor(() => {
+    expect(screen.getByText('rendered:Number')).toBeInTheDocument();
+  });
+});
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Table/VirtualTable.tsx b/superset-frontend/packages/superset-ui-core/src/components/Table/VirtualTable.tsx
index 1d5ada1948b..53ef9f30e0a 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/Table/VirtualTable.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/Table/VirtualTable.tsx
@@ -24,10 +24,14 @@ import {
 } from 'antd/es/table';
 import classNames from 'classnames';
 import { useResizeDetector } from 'react-resize-detector';
-import { useEffect, useRef, useState, useCallback, CSSProperties } from 'react';
-import { VariableSizeGrid as Grid } from 'react-window';
+import { useRef, useState, useCallback, type UIEvent } from 'react';
+import {
+  Grid,
+  type CellComponentProps,
+  type GridImperativeAPI,
+} from 'react-window';
 import { safeHtmlSpan } from '@superset-ui/core';
-import { useTheme, styled } from '@apache-superset/core/theme';
+import { useTheme, styled, SupersetTheme } from '@apache-superset/core/theme';
 
 import { TableSize, ETableAction } from './index';
 
@@ -71,6 +75,59 @@ const StyledTable = styled(AntTable)(
 const SMALL = 39;
 const MIDDLE = 47;
 
+interface VirtualGridCellProps {
+  mergedColumns: AntTableProps<any>['columns'];
+  rawData: readonly object[];
+  cellSize: number;
+  allowHTML: boolean;
+  theme: SupersetTheme;
+}
+
+// Rendered via `cellComponent`, so it must be a stable reference (module scope)
+// rather than defined inline on every render of the enclosing table -
+// otherwise react-window would treat it as a new component type each render
+// and remount every cell. All the data it needs is threaded through
+// `cellProps` instead of being closed over.
+const VirtualGridCell = ({
+  columnIndex,
+  rowIndex,
+  style,
+  mergedColumns,
+  rawData,
+  cellSize,
+  allowHTML,
+  theme,
+}: CellComponentProps<VirtualGridCellProps>) => {
+  const data: any = rawData?.[rowIndex];
+  // Set default content
+  let content = data?.[(mergedColumns as any)?.[columnIndex]?.dataIndex];
+  // Check if the column has a render function
+  const render = mergedColumns?.[columnIndex]?.render;
+  if (typeof render === 'function') {
+    // Use render function to generate formatted content using column's render function
+    content = render(content, data, rowIndex);
+  }
+
+  if (allowHTML && typeof content === 'string') {
+    content = safeHtmlSpan(content);
+  }
+
+  return (
+    <StyledCell
+      className={classNames('virtual-table-cell', {
+        'virtual-table-cell-last':
+          columnIndex === (mergedColumns?.length ?? 0) - 1,
+      })}
+      style={style}
+      title={typeof content === 'string' ? content : undefined}
+      theme={theme}
+      height={cellSize}
+    >
+      {content}
+    </StyledCell>
+  );
+};
+
 const VirtualTable = <RecordType extends object>(
   props: VirtualTableProps<RecordType>,
 ) => {
@@ -126,19 +183,15 @@ const VirtualTable = <RecordType extends object>(
       (lastColumn.width as number) + Math.floor(tableWidth - totalWidth);
   }
 
-  const gridRef = useRef<any>();
+  const gridRef = useRef<GridImperativeAPI>(null);
   const [connectObject] = useState<any>(() => {
     const obj = {};
     Object.defineProperty(obj, 'scrollLeft', {
-      get: () => {
-        if (gridRef.current) {
-          return gridRef.current?.state?.scrollLeft;
-        }
-        return 0;
-      },
+      get: () => gridRef.current?.element?.scrollLeft ?? 0,
       set: (scrollLeft: number) => {
-        if (gridRef.current) {
-          gridRef.current.scrollTo({ scrollLeft });
+        const element = gridRef.current?.element;
+        if (element) {
+          element.scrollLeft = scrollLeft;
         }
       },
     });
@@ -146,14 +199,11 @@ const VirtualTable = <RecordType extends object>(
     return obj;
   });
 
-  const resetVirtualGrid = () => {
-    gridRef.current?.resetAfterIndices({
-      columnIndex: 0,
-      shouldForceUpdate: true,
-    });
-  };
-
-  useEffect(() => resetVirtualGrid, [tableWidth, columns, size]);
+  // No manual cache-reset is needed here (react-window v2 has no
+  // `resetAfterIndices`-style API): `columnWidth` below is a fresh inline
+  // closure over `mergedColumns` on every render, so react-window's internal
+  // size cache - which is invalidated whenever the `columnWidth`/`rowHeight`
+  // function reference changes - recomputes automatically.
 
   /*
    * antd Table has a runtime error when it tries to fire the onChange event triggered from a pageChange
@@ -166,7 +216,10 @@ const VirtualTable = <RecordType extends object>(
      * We intentionally leave horizontal scroll where it was so user can focus on
      * specific range of columns as they page through data
      */
-    gridRef.current?.scrollTo?.({ scrollTop: 0 });
+    const element = gridRef.current?.element;
+    if (element) {
+      element.scrollTop = 0;
+    }
 
     onChange?.(
       {
@@ -192,61 +245,31 @@ const VirtualTable = <RecordType extends object>(
     const cellSize = size === TableSize.Middle ? MIDDLE : SMALL;
     return (
       <Grid
-        ref={gridRef}
+        gridRef={gridRef}
         className="virtual-grid"
         columnCount={mergedColumns.length}
         columnWidth={(index: number) => {
           const { width = DEFAULT_COL_WIDTH } = mergedColumns[index];
           return width as number;
         }}
-        height={height || (scroll!.y as number)}
         rowCount={rawData.length}
         rowHeight={() => cellSize}
-        width={tableWidth}
-        onScroll={({ scrollLeft }: { scrollLeft: number }) => {
-          onScroll({ scrollLeft });
+        style={{
+          height: height || (scroll!.y as number),
+          width: tableWidth,
         }}
-      >
-        {({
-          columnIndex,
-          rowIndex,
-          style,
-        }: {
-          columnIndex: number;
-          rowIndex: number;
-          style: CSSProperties;
-        }) => {
-          const data: any = rawData?.[rowIndex];
-          // Set default content
-          let content =
-            data?.[(mergedColumns as any)?.[columnIndex]?.dataIndex];
-          // Check if the column has a render function
-          const render = mergedColumns[columnIndex]?.render;
-          if (typeof render === 'function') {
-            // Use render function to generate formatted content using column's render function
-            content = render(content, data, rowIndex);
-          }
-
-          if (allowHTML && typeof content === 'string') {
-            content = safeHtmlSpan(content);
-          }
-
-          return (
-            <StyledCell
-              className={classNames('virtual-table-cell', {
-                'virtual-table-cell-last':
-                  columnIndex === mergedColumns.length - 1,
-              })}
-              style={style}
-              title={typeof content === 'string' ? content : undefined}
-              theme={theme}
-              height={cellSize}
-            >
-              {content}
-            </StyledCell>
-          );
+        cellComponent={VirtualGridCell}
+        cellProps={{
+          mergedColumns,
+          rawData,
+          cellSize,
+          allowHTML,
+          theme,
         }}
-      </Grid>
+        onScroll={(event: UIEvent<HTMLDivElement>) => {
+          onScroll({ scrollLeft: event.currentTarget.scrollLeft });
+        }}
+      />
     );
   };
 
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.tsx
index 9001656f02a..f031a28c5f1 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.tsx
@@ -18,7 +18,6 @@
  */
 
 import {
-  CSSProperties,
   ReactNode,
   useCallback,
   useEffect,
@@ -44,7 +43,7 @@ import {
   Icons,
 } from '@superset-ui/core/components';
 import { debounce } from 'lodash-es';
-import { FixedSizeList as List } from 'react-window';
+import { List, type RowComponentProps } from 'react-window';
 import { InputRef } from 'antd';
 import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
 import { VirtualizedMenuItem } from '../MenuItemWithTruncation';
@@ -53,6 +52,34 @@ import { Dataset } from '../types';
 const SUBMENU_HEIGHT = 200;
 const SHOW_COLUMNS_SEARCH_THRESHOLD = 10;
 
+interface DrillByColumnRowProps {
+  columns: Column[];
+  onSelectColumn: (event: React.MouseEvent, column: Column) => void;
+}
+
+// Rendered via `rowComponent`, so it must be a stable reference (module
+// scope) rather than defined inline on every render of the submenu -
+// otherwise react-window would treat it as a new component type each
+// render and remount every row. All the data it needs is threaded
+// through `rowProps` instead of being closed over.
+function DrillByColumnRow({
+  index,
+  style,
+  columns,
+  onSelectColumn,
+}: RowComponentProps<DrillByColumnRowProps>) {
+  const column = columns[index];
+  return (
+    <VirtualizedMenuItem
+      tooltipText={column.verbose_name || column.column_name}
+      onClick={e => onSelectColumn(e, column)}
+      style={style}
+    >
+      {column.verbose_name || column.column_name}
+    </VirtualizedMenuItem>
+  );
+}
+
 export interface DrillBySubmenuProps {
   drillByConfig?: ContextMenuFilters['drillBy'];
   formData: BaseFormData & { [key: string]: any };
@@ -190,26 +217,9 @@ export const DrillBySubmenu = ({
 
   const isDisabled = !handlesDimensionContextMenu || !hasDrillBy;
 
-  const Row = ({
-    index,
-    data,
-    style,
-  }: {
-    index: number;
-    data: { columns: Column[] };
-    style: CSSProperties;
-  }) => {
-    const { columns } = data;
-    const column = columns[index];
-    return (
-      <VirtualizedMenuItem
-        tooltipText={column.verbose_name || column.column_name}
-        onClick={e => handleSelection(e, column)}
-        style={style}
-      >
-        {column.verbose_name || column.column_name}
-      </VirtualizedMenuItem>
-    );
+  const listRowProps: DrillByColumnRowProps = {
+    columns: filteredColumns,
+    onSelectColumn: handleSelection,
   };
 
   const popoverContent = (
@@ -260,15 +270,13 @@ export const DrillBySubmenu = ({
         </div>
       ) : filteredColumns.length ? (
         <List
-          width="100%"
-          height={SUBMENU_HEIGHT}
-          itemSize={35}
-          itemCount={filteredColumns.length}
-          itemData={{ columns: filteredColumns }}
+          style={{ width: '100%', height: SUBMENU_HEIGHT }}
+          rowHeight={35}
+          rowCount={filteredColumns.length}
+          rowProps={listRowProps}
+          rowComponent={DrillByColumnRow}
           overscanCount={20}
-        >
-          {Row}
-        </List>
+        />
       ) : (
         <div
           css={css`
diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/FoldersEditor.test.tsx b/superset-frontend/src/components/Datasource/FoldersEditor/FoldersEditor.test.tsx
index a8b9a0c5cb7..6835392b73e 100644
--- a/superset-frontend/src/components/Datasource/FoldersEditor/FoldersEditor.test.tsx
+++ b/superset-frontend/src/components/Datasource/FoldersEditor/FoldersEditor.test.tsx
@@ -45,28 +45,26 @@ jest.mock(
       children({ height: 500, width: 400 }),
 );
 
-// Mock react-window VariableSizeList to render all items for testing
+// Mock react-window List to render all items for testing
 jest.mock('react-window', () => ({
-  VariableSizeList: ({
-    children: Row,
-    itemCount,
-    itemData,
+  List: ({
+    rowComponent: Row,
+    rowCount,
+    rowProps,
   }: {
-    children: React.ComponentType<{
-      index: number;
-      style: React.CSSProperties;
-      data: unknown;
-    }>;
-    itemCount: number;
-    itemData: unknown;
+    rowComponent: React.ComponentType<
+      { index: number; style: React.CSSProperties } & Record<string, unknown>
+    >;
+    rowCount: number;
+    rowProps: Record<string, unknown>;
   }) => (
     <div data-testid="virtualized-list">
-      {Array.from({ length: itemCount }, (_, index) => (
+      {Array.from({ length: rowCount }, (_, index) => (
         <Row
           key={index}
           index={index}
           style={{ height: 'auto', position: 'relative' }}
-          data={itemData}
+          {...rowProps}
         />
       ))}
     </div>
diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeItem.tsx b/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeItem.tsx
index 9bfda10c8a3..bd0a373fb04 100644
--- a/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeItem.tsx
+++ b/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeItem.tsx
@@ -18,7 +18,7 @@
  */
 
 import { CSSProperties, memo } from 'react';
-import type { ListChildComponentProps } from 'react-window';
+import type { RowComponentProps } from 'react-window';
 import { useDroppable } from '@dnd-kit/core';
 import type { UniqueIdentifier } from '@dnd-kit/core';
 import type { Metric, ColumnMeta } from '@superset-ui/chart-controls';
@@ -135,32 +135,34 @@ const TreeItemWrapper = memo(function TreeItemWrapper({
   );
 });
 
-function VirtualizedTreeItemComponent({
+// Not wrapped in `memo()` here: react-window v2 already wraps `rowComponent`
+// in its own `memo()` internally (comparing `ariaAttributes`/`style`/the
+// rest of the row props), so an outer `memo()` would be redundant - and
+// React 19's `memo()` typings widen the wrapped component's return type to
+// `ReactNode`, which react-window v2's `rowComponent` type (expecting
+// `ReactElement | null`) rejects.
+export function VirtualizedTreeItem({
   index,
   style,
-  data,
-}: ListChildComponentProps<VirtualizedTreeItemData>) {
-  const {
-    flattenedItems,
-    collapsedIds,
-    selectedItemIds,
-    editingFolderId,
-    folderChildCounts,
-    itemSeparatorInfo,
-    visibleItemIds,
-    searchTerm,
-    metricsMap,
-    columnsMap,
-    activeId,
-    draggedFolderChildIds,
-    forbiddenDropFolderIds,
-    currentDropTargetId,
-    onToggleCollapse,
-    onSelect,
-    onStartEdit,
-    onFinishEdit,
-  } = data;
-
+  flattenedItems,
+  collapsedIds,
+  selectedItemIds,
+  editingFolderId,
+  folderChildCounts,
+  itemSeparatorInfo,
+  visibleItemIds,
+  searchTerm,
+  metricsMap,
+  columnsMap,
+  activeId,
+  draggedFolderChildIds,
+  forbiddenDropFolderIds,
+  currentDropTargetId,
+  onToggleCollapse,
+  onSelect,
+  onStartEdit,
+  onFinishEdit,
+}: RowComponentProps<VirtualizedTreeItemData>) {
   const item = flattenedItems[index];
 
   if (!item) {
@@ -224,5 +226,3 @@ function VirtualizedTreeItemComponent({
     />
   );
 }
-
-export const VirtualizedTreeItem = memo(VirtualizedTreeItemComponent);
diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx b/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx
index eeb2dad76b5..0f27036b0fb 100644
--- a/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx
+++ b/superset-frontend/src/components/Datasource/FoldersEditor/VirtualizedTreeList.tsx
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-import { useCallback, useEffect, useMemo, useRef } from 'react';
-import { VariableSizeList as List } from 'react-window';
+import { useCallback, useMemo, useRef } from 'react';
+import { List, type ListImperativeAPI } from 'react-window';
 import type { UniqueIdentifier } from '@dnd-kit/core';
 import type { Metric, ColumnMeta } from '@superset-ui/chart-controls';
 import { FoldersEditorItemType } from '../types';
@@ -82,7 +82,7 @@ export function VirtualizedTreeList({
   onStartEdit,
   onFinishEdit,
 }: VirtualizedTreeListProps) {
-  const listRef = useRef<List>(null);
+  const listRef = useRef<ListImperativeAPI>(null);
   const containerRef = useRef<HTMLDivElement>(null);
 
   // Custom auto-scroll during drag (replaces dnd-kit's auto-scroll which conflicts with virtualization)
@@ -93,20 +93,15 @@ export function VirtualizedTreeList({
     listHeight: height,
   });
 
-  // Reset list cache when items structure changes, but not during drag
-  // Resetting during drag causes jumping/flickering
-  useEffect(() => {
-    if (!isDragging) {
-      listRef.current?.resetAfterIndex(0);
-    }
-  }, [
-    flattenedItems,
-    collapsedIds,
-    folderChildCounts,
-    itemSeparatorInfo,
-    visibleItemIds,
-    isDragging,
-  ]);
+  // No manual cache-reset is needed here (react-window v2 has no
+  // `resetAfterIndex`-style API): `getItemSize` below is a `useCallback`
+  // whose dependency list matches everything that can change row heights,
+  // so react-window's internal size cache - which is invalidated whenever
+  // the `rowHeight` function reference changes - recomputes automatically.
+  // Because `flattenedItems` is held stable during drag (see
+  // useDragHandlers), `getItemSize`'s reference - and therefore the cache -
+  // naturally stays stable during drag too, preserving the old "don't
+  // reset while dragging" behavior without needing an explicit gate.
 
   // Calculate item size for react-window
   const getItemSize = useCallback(
@@ -169,7 +164,7 @@ export function VirtualizedTreeList({
   );
 
   // Prepare item data for the row renderer
-  const itemData: VirtualizedTreeItemData = useMemo(
+  const rowProps: VirtualizedTreeItemData = useMemo(
     () => ({
       flattenedItems,
       collapsedIds,
@@ -218,16 +213,14 @@ export function VirtualizedTreeList({
   return (
     <div ref={containerRef} style={{ width, height, position: 'relative' }}>
       <List
-        ref={listRef}
-        width={width}
-        height={height}
-        itemCount={flattenedItems.length}
-        itemSize={getItemSize}
-        itemData={itemData}
+        listRef={listRef}
+        style={{ width, height }}
+        rowCount={flattenedItems.length}
+        rowHeight={getItemSize}
+        rowProps={rowProps}
+        rowComponent={VirtualizedTreeItem}
         overscanCount={overscanCount}
-      >
-        {VirtualizedTreeItem}
-      </List>
+      />
     </div>
   );
 }
diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useAutoScroll.ts b/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useAutoScroll.ts
index a9da33039d5..1e30affd771 100644
--- a/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useAutoScroll.ts
+++ b/superset-frontend/src/components/Datasource/FoldersEditor/hooks/useAutoScroll.ts
@@ -18,7 +18,7 @@
  */
 
 import { useEffect, useRef } from 'react';
-import type { VariableSizeList as List } from 'react-window';
+import type { ListImperativeAPI } from 'react-window';
 
 // Distance from edge where auto-scroll activates (in pixels)
 const SCROLL_THRESHOLD = 80;
@@ -28,7 +28,7 @@ const BASE_SCROLL_SPEED = 8;
 const MAX_SPEED_MULTIPLIER = 3;
 
 interface UseAutoScrollOptions {
-  listRef: React.RefObject<List>;
+  listRef: React.RefObject<ListImperativeAPI | null>;
   containerRef: React.RefObject<HTMLDivElement>;
   isDragging: boolean;
   listHeight: number;
@@ -110,7 +110,7 @@ export function useAutoScroll({
     // Animation frame callback - uses time-based scrolling for consistent speed
     const scrollFrame = (currentTime: number) => {
       const list = listRef.current;
-      const outerElement = (list as any)?._outerRef;
+      const outerElement = list?.element;
 
       if (!list || !outerElement || !state.direction) {
         // Restore pointer events when scrolling stops
@@ -149,7 +149,7 @@ export function useAutoScroll({
 
       if (Math.abs(newScroll - currentScroll) > 0.5) {
         // Use direct DOM manipulation for smoother scrolling
-        // react-window's scrollTo triggers re-renders which can cause stutter
+        // react-window's scrollToRow triggers re-renders which can cause stutter
         outerElement.scrollTop = newScroll;
       }
 
diff --git a/superset-frontend/src/dashboard/components/SliceAdder.test.tsx b/superset-frontend/src/dashboard/components/SliceAdder.test.tsx
index 9cdac1de085..9b34aaa769b 100644
--- a/superset-frontend/src/dashboard/components/SliceAdder.test.tsx
+++ b/superset-frontend/src/dashboard/components/SliceAdder.test.tsx
@@ -25,7 +25,11 @@ import {
 import { DatasourceType } from '@superset-ui/core';
 import { sliceEntitiesForDashboard as mockSliceEntities } from 'spec/fixtures/mockSliceEntities';
 import { configureStore } from '@reduxjs/toolkit';
-import SliceAdder, { SliceAdderProps, sortByComparator } from './SliceAdder';
+import SliceAdder, {
+  SliceAdderProps,
+  sortByComparator,
+  SliceListRow,
+} from './SliceAdder';
 
 // Mock the Select component to avoid debounce issues
 jest.mock('@superset-ui/core', () => ({
@@ -236,4 +240,54 @@ describe('SliceAdder', () => {
     // Verify the internal state was updated by checking if new charts are available
     expect(screen.getByRole('checkbox')).toBeInTheDocument();
   });
+
+  // Covers the react-window v2 `rowComponent`/`rowProps` wiring: SliceAdder
+  // hoists its row renderer to module scope (see SliceAdder.tsx) so
+  // react-window doesn't remount every row on each render. This verifies
+  // that hoisted row renders correctly from the flattened rowProps react-window
+  // passes it (index/style plus the custom row props), rather than through
+  // closures over component state.
+  test('SliceListRow renders a chart card for the slice at the given index', () => {
+    const filteredSlices = Object.values(mockSliceEntities.slices);
+    render(
+      <SliceListRow
+        index={0}
+        style={{}}
+        ariaAttributes={{
+          role: 'listitem',
+          'aria-posinset': 1,
+          'aria-setsize': filteredSlices.length,
+        }}
+        filteredSlices={filteredSlices}
+        selectedSliceIdsSet={new Set()}
+        editMode={false}
+      />,
+      { useDnd: true, store: mockStore },
+    );
+    expect(screen.getByTestId('chart-card')).toBeInTheDocument();
+    expect(screen.getByText(filteredSlices[0].slice_name)).toBeInTheDocument();
+  });
+
+  test('SliceListRow renders the slice at the row index passed via rowProps, not index 0', () => {
+    const filteredSlices = Object.values(mockSliceEntities.slices);
+    render(
+      <SliceListRow
+        index={1}
+        style={{}}
+        ariaAttributes={{
+          role: 'listitem',
+          'aria-posinset': 2,
+          'aria-setsize': filteredSlices.length,
+        }}
+        filteredSlices={filteredSlices}
+        selectedSliceIdsSet={new Set()}
+        editMode={false}
+      />,
+      { useDnd: true, store: mockStore },
+    );
+    expect(screen.getByText(filteredSlices[1].slice_name)).toBeInTheDocument();
+    expect(
+      screen.queryByText(filteredSlices[0].slice_name),
+    ).not.toBeInTheDocument();
+  });
 });
diff --git a/superset-frontend/src/dashboard/components/SliceAdder.tsx b/superset-frontend/src/dashboard/components/SliceAdder.tsx
index 5abc643ed8f..f260bc0a2d5 100644
--- a/superset-frontend/src/dashboard/components/SliceAdder.tsx
+++ b/superset-frontend/src/dashboard/components/SliceAdder.tsx
@@ -19,7 +19,7 @@
 /* eslint-env browser */
 import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
 import AutoSizer from 'react-virtualized-auto-sizer';
-import { FixedSizeList as List } from 'react-window';
+import { List, type RowComponentProps } from 'react-window';
 // @ts-expect-error
 import { createFilter } from 'react-search-input';
 import { t } from '@apache-superset/core/translation';
@@ -167,6 +167,71 @@ function getFilteredSortedSlices(
     .sort(sortByComparator(sortBy));
 }
 
+export interface SliceListRowProps {
+  filteredSlices: Slice[];
+  selectedSliceIdsSet: Set<number>;
+  editMode: boolean;
+}
+
+// Rendered via `rowComponent`, so it must be a stable reference (module
+// scope) rather than defined inline on every render of SliceAdder -
+// otherwise react-window would treat it as a new component type each
+// render and remount every row (losing in-flight drag state). All the
+// data it needs is threaded through `rowProps` instead of being closed
+// over.
+export function SliceListRow({
+  index,
+  style,
+  filteredSlices,
+  selectedSliceIdsSet,
+  editMode,
+}: RowComponentProps<SliceListRowProps>) {
+  const cellData = filteredSlices[index];
+
+  const isSelected = selectedSliceIdsSet.has(cellData.slice_id);
+  const type = CHART_TYPE;
+  const id = NEW_CHART_ID;
+
+  const meta = {
+    chartId: cellData.slice_id,
+    sliceName: cellData.slice_name,
+  };
+  return (
+    <DragDroppable
+      key={cellData.slice_id}
+      component={{ type, id, meta }}
+      parentComponent={{
+        id: NEW_COMPONENTS_SOURCE_ID,
+        type: NEW_COMPONENT_SOURCE_TYPE,
+      }}
+      index={index}
+      depth={0}
+      disableDragDrop={isSelected}
+      editMode={editMode}
+      // we must use a custom drag preview within the List because
+      // it does not seem to work within a fixed-position container
+      useEmptyDragPreview
+      // List library expect style props here
+      // actual style should be applied to nested AddSliceCard component
+      style={{}}
+    >
+      {({ dragSourceRef }: { dragSourceRef: ConnectDragSource }) => (
+        <AddSliceCard
+          innerRef={dragSourceRef}
+          style={style}
+          sliceName={cellData.slice_name}
+          lastModified={cellData.changed_on_humanized}
+          visType={cellData.viz_type}
+          datasourceUrl={cellData.datasource_url}
+          datasourceName={cellData.datasource_name}
+          thumbnailUrl={cellData.thumbnail_url}
+          isSelected={isSelected}
+        />
+      )}
+    </DragDroppable>
+  );
+}
+
 function SliceAdder({
   fetchSlices,
   updateSlices,
@@ -321,53 +386,8 @@ function SliceAdder({
     [fetchSlices, searchTerm, sortBy],
   );
 
-  const rowRenderer = useCallback(
-    ({ index, style }: { index: number; style: React.CSSProperties }) => {
-      const cellData = filteredSlices[index];
-
-      const isSelected = selectedSliceIdsSet.has(cellData.slice_id);
-      const type = CHART_TYPE;
-      const id = NEW_CHART_ID;
-
-      const meta = {
-        chartId: cellData.slice_id,
-        sliceName: cellData.slice_name,
-      };
-      return (
-        <DragDroppable
-          key={cellData.slice_id}
-          component={{ type, id, meta }}
-          parentComponent={{
-            id: NEW_COMPONENTS_SOURCE_ID,
-            type: NEW_COMPONENT_SOURCE_TYPE,
-          }}
-          index={index}
-          depth={0}
-          disableDragDrop={isSelected}
-          editMode={editMode}
-          // we must use a custom drag preview within the List because
-          // it does not seem to work within a fixed-position container
-          useEmptyDragPreview
-          // List library expect style props here
-          // actual style should be applied to nested AddSliceCard component
-          style={{}}
-        >
-          {({ dragSourceRef }: { dragSourceRef: ConnectDragSource }) => (
-            <AddSliceCard
-              innerRef={dragSourceRef}
-              style={style}
-              sliceName={cellData.slice_name}
-              lastModified={cellData.changed_on_humanized}
-              visType={cellData.viz_type}
-              datasourceUrl={cellData.datasource_url}
-              datasourceName={cellData.datasource_name}
-              thumbnailUrl={cellData.thumbnail_url}
-              isSelected={isSelected}
-            />
-          )}
-        </DragDroppable>
-      );
-    },
+  const listRowProps = useMemo<SliceListRowProps>(
+    () => ({ filteredSlices, selectedSliceIdsSet, editMode }),
     [filteredSlices, selectedSliceIdsSet, editMode],
   );
 
@@ -448,14 +468,13 @@ function SliceAdder({
           <AutoSizer>
             {({ height, width }: { height: number; width: number }) => (
               <List
-                width={width}
-                height={height}
-                itemCount={filteredSlices.length}
-                itemSize={DEFAULT_CELL_HEIGHT}
-                itemKey={index => filteredSlices[index].slice_id}
-              >
-                {rowRenderer}
-              </List>
+                style={{ width, height }}
+                rowCount={filteredSlices.length}
+                rowHeight={DEFAULT_CELL_HEIGHT}
+                rowProps={listRowProps}
+                rowComponent={SliceListRow}
+                rowKey={(index, data) => data.filteredSlices[index].slice_id}
+              />
             )}
           </AutoSizer>
         </ChartList>
diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourceItems.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourceItems.tsx
index 53e55b60543..8da7586f9c0 100644
--- a/superset-frontend/src/explore/components/DatasourcePanel/DatasourceItems.tsx
+++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourceItems.tsx
@@ -16,8 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { VariableSizeList as List } from 'react-window';
+import { useCallback, useMemo, useState } from 'react';
+import { List } from 'react-window';
 import { FlattenedItem, Folder } from './types';
 import DatasourcePanelItem from './DatasourcePanelItem';
 
@@ -98,7 +98,6 @@ export const DatasourceItems = ({
   height,
   folders,
 }: DatasourceItemsProps) => {
-  const listRef = useRef<List>(null);
   const [collapsedFolderIds, setCollapsedFolderIds] = useState<Set<string>>(
     new Set(
       folders.filter(folder => folder.isCollapsed).map(folder => folder.id),
@@ -122,17 +121,15 @@ export const DatasourceItems = ({
     });
   }, []);
 
-  useEffect(() => {
-    // reset the list cache when flattenedItems length changes to recalculate the heights
-    listRef.current?.resetAfterIndex(0);
-  }, [flattenedItems]);
-
-  const getItemSize = useCallback(
+  // No manual cache-reset is needed: `rowHeight` is a useCallback tied to
+  // `flattenedItems`, so react-window's internal size cache recomputes
+  // automatically whenever its reference changes.
+  const rowHeight = useCallback(
     (index: number) => flattenedItems[index].height,
     [flattenedItems],
   );
 
-  const itemData = useMemo(
+  const rowProps = useMemo(
     () => ({
       flattenedItems,
       folderMap,
@@ -151,15 +148,12 @@ export const DatasourceItems = ({
 
   return (
     <List
-      ref={listRef}
-      width={width - BORDER_WIDTH}
-      height={height}
-      itemSize={getItemSize}
-      itemCount={flattenedItems.length}
-      itemData={itemData}
+      style={{ width: width - BORDER_WIDTH, height }}
+      rowHeight={rowHeight}
+      rowCount={flattenedItems.length}
+      rowProps={rowProps}
+      rowComponent={DatasourcePanelItem}
       overscanCount={5}
-    >
-      {DatasourcePanelItem}
-    </List>
+    />
   );
 };
diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx
index 37449902b5e..f3859a9f768 100644
--- a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx
+++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx
@@ -22,12 +22,12 @@ import {
 } from 'src/explore/components/DatasourcePanel/fixtures';
 import { screen, userEvent, render } from 'spec/helpers/testing-library';
 import DatasourcePanelItem, {
-  DatasourcePanelItemProps,
+  DatasourcePanelItemRowProps,
 } from './DatasourcePanelItem';
 import { FoldersEditorItemType } from 'src/components/Datasource/types';
 import { MetricItem, ColumnItem } from './types';
 
-const mockData: DatasourcePanelItemProps['data'] = {
+const mockData: DatasourcePanelItemRowProps = {
   flattenedItems: [
     { type: 'header', depth: 0, folderId: '1', height: 50 },
     ...metrics.map((m, idx) => ({
@@ -82,11 +82,22 @@ const mockData: DatasourcePanelItemProps['data'] = {
   collapsedFolderIds: new Set(),
 };
 
-const setup = (data: DatasourcePanelItemProps['data'] = mockData) =>
+const setup = (data: DatasourcePanelItemRowProps = mockData) =>
   render(
     <>
       {data.flattenedItems.map((_, index) => (
-        <DatasourcePanelItem index={index} data={data} style={{}} />
+        <DatasourcePanelItem
+          // eslint-disable-next-line react/no-array-index-key -- test fixture has no stable id
+          key={index}
+          index={index}
+          style={{}}
+          ariaAttributes={{
+            role: 'listitem',
+            'aria-posinset': index + 1,
+            'aria-setsize': data.flattenedItems.length,
+          }}
+          {...data}
+        />
       ))}
     </>,
     { useDnd: true, useRedux: true, initialState: { explore: {} } },
diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx
index c19753eaa3d..fed70cdab93 100644
--- a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx
+++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx
@@ -16,7 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { CSSProperties, ReactNode, useCallback } from 'react';
+import { ReactNode, useCallback } from 'react';
+import type { RowComponentProps } from 'react-window';
 
 import { t } from '@apache-superset/core/translation';
 import { useCSSTextTruncation } from '@superset-ui/core';
@@ -112,30 +113,26 @@ const Divider = styled.div`
   `}
 `;
 
-export interface DatasourcePanelItemProps {
-  index: number;
-  style: CSSProperties;
-  data: {
-    flattenedItems: FlattenedItem[];
-    folderMap: Map<string, Folder>;
-    width: number;
-    onToggleCollapse: (folderId: string) => void;
-    collapsedFolderIds: Set<string>;
-  };
+export interface DatasourcePanelItemRowProps {
+  flattenedItems: FlattenedItem[];
+  folderMap: Map<string, Folder>;
+  width: number;
+  onToggleCollapse: (folderId: string) => void;
+  collapsedFolderIds: Set<string>;
 }
 
+export type DatasourcePanelItemProps =
+  RowComponentProps<DatasourcePanelItemRowProps>;
+
 const DatasourcePanelItem = ({
   index,
   style,
-  data,
+  flattenedItems,
+  folderMap,
+  width,
+  onToggleCollapse,
+  collapsedFolderIds,
 }: DatasourcePanelItemProps) => {
-  const {
-    flattenedItems,
-    folderMap,
-    width,
-    onToggleCollapse,
-    collapsedFolderIds,
-  } = data;
   const item = flattenedItems[index];
   const theme = useTheme();
   const [labelRef, labelIsTruncated] = useCSSTextTruncation<HTMLSpanElement>({