diff --git a/frontend/src/metabase/components/VirtualizedList.jsx b/frontend/src/metabase/components/VirtualizedList.tsx similarity index 69% rename from frontend/src/metabase/components/VirtualizedList.jsx rename to frontend/src/metabase/components/VirtualizedList.tsx index 7f8104e36f00997a64310e03d76f5bf390d2ea27..acd498d2a2eafdc8c7b5dde432e9940d44a7f6c0 100644 --- a/frontend/src/metabase/components/VirtualizedList.jsx +++ b/frontend/src/metabase/components/VirtualizedList.tsx @@ -1,8 +1,20 @@ -/* eslint-disable react/prop-types */ import React from "react"; import { List, WindowScroller, AutoSizer } from "react-virtualized"; -function VirtualizedList({ items, rowHeight, renderItem, scrollElement }) { +export interface VirtualizedListProps<Item = unknown> + extends React.HTMLProps<HTMLUListElement> { + items: Item[]; + rowHeight: number; + renderItem: (props: { item: Item; index: number }) => React.ReactNode; + scrollElement?: HTMLElement | null; +} + +function VirtualizedList<Item>({ + items, + rowHeight, + renderItem, + scrollElement, +}: VirtualizedListProps<Item>) { const rowRenderer = React.useCallback( ({ index, key, style }) => ( <div key={key} style={style}> @@ -15,7 +27,7 @@ function VirtualizedList({ items, rowHeight, renderItem, scrollElement }) { const renderScrollComponent = React.useCallback( ({ width }) => { return ( - <WindowScroller scrollElement={scrollElement}> + <WindowScroller scrollElement={scrollElement || undefined}> {({ height, isScrolling, scrollTop }) => ( <List autoHeight diff --git a/frontend/src/metabase/containers/DataPicker/CardPicker/CardPicker.styled.tsx b/frontend/src/metabase/containers/DataPicker/CardPicker/CardPicker.styled.tsx index 5bb3ffb3db8ab4bdc3032ee8c2efa6fe9ff9a7fa..4c5897c86f1578db3060a45c41ca92f82dd8253f 100644 --- a/frontend/src/metabase/containers/DataPicker/CardPicker/CardPicker.styled.tsx +++ b/frontend/src/metabase/containers/DataPicker/CardPicker/CardPicker.styled.tsx @@ -1,8 +1,6 @@ import styled from "@emotion/styled"; -import SelectList from "metabase/components/SelectList"; - -export const StyledSelectList = styled(SelectList)` +export const ListContainer = styled.div` width: 100%; padding-left: 1rem; `; diff --git a/frontend/src/metabase/containers/DataPicker/CardPicker/CardPickerView.tsx b/frontend/src/metabase/containers/DataPicker/CardPicker/CardPickerView.tsx index b301fd7e472bdf40d46edf8d10a00f952635af93..cb7a60925e7ace19c3923a299c99f37241828b63 100644 --- a/frontend/src/metabase/containers/DataPicker/CardPicker/CardPickerView.tsx +++ b/frontend/src/metabase/containers/DataPicker/CardPicker/CardPickerView.tsx @@ -1,8 +1,6 @@ import React, { useCallback, useMemo } from "react"; import _ from "underscore"; -import SelectList from "metabase/components/SelectList"; - import { canonicalCollectionId } from "metabase/collections/utils"; import type { ITreeNodeItem } from "metabase/components/tree/types"; @@ -13,9 +11,10 @@ import type { DataPickerSelectedItem, VirtualTable } from "../types"; import EmptyState from "../EmptyState"; import LoadingState from "../LoadingState"; +import VirtualizedSelectList from "../VirtualizedSelectList"; import PanePicker from "../PanePicker"; -import { StyledSelectList } from "./CardPicker.styled"; +import { ListContainer } from "./CardPicker.styled"; type TargetModel = "model" | "question"; @@ -55,7 +54,7 @@ function TableSelectListItem({ onSelect: (id: Table["id"]) => void; }) { return ( - <SelectList.Item + <VirtualizedSelectList.Item id={table.id} name={table.display_name} isSelected={isSelected} @@ -63,7 +62,7 @@ function TableSelectListItem({ onSelect={onSelect} > {table.display_name} - </SelectList.Item> + </VirtualizedSelectList.Item> ); } @@ -104,7 +103,7 @@ function CardPickerView({ ); const renderVirtualTable = useCallback( - (table: VirtualTable) => ( + ({ item: table }: { item: VirtualTable }) => ( <TableSelectListItem key={table.id} table={table} @@ -130,9 +129,14 @@ function CardPickerView({ ) : isEmpty ? ( <EmptyState /> ) : ( - <StyledSelectList> - {virtualTables?.map?.(renderVirtualTable)} - </StyledSelectList> + <ListContainer> + {Array.isArray(virtualTables) && ( + <VirtualizedSelectList<VirtualTable> + items={virtualTables} + renderItem={renderVirtualTable} + /> + )} + </ListContainer> )} </PanePicker> ); diff --git a/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPicker.styled.tsx b/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPicker.styled.tsx index 98464679d166be1285e4109fc3d4c94bac1ac4b8..fb0f876d87113cfa399abe591eee7f75c672c76e 100644 --- a/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPicker.styled.tsx +++ b/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPicker.styled.tsx @@ -1,8 +1,6 @@ import styled from "@emotion/styled"; -import SelectList from "metabase/components/SelectList"; - -export const StyledSelectList = styled(SelectList)` +export const ListContainer = styled.div` width: 100%; padding: 0 1rem; `; diff --git a/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPickerView.tsx b/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPickerView.tsx index cd741fc35e5817b1cd2c7e9f18324db0faf0f849..c741e5d92deba123e5672c2638d7c023b4a3331d 100644 --- a/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPickerView.tsx +++ b/frontend/src/metabase/containers/DataPicker/RawDataPicker/RawDataPickerView.tsx @@ -1,8 +1,6 @@ import React, { useCallback, useMemo } from "react"; import _ from "underscore"; -import SelectList from "metabase/components/SelectList"; - import type { ITreeNodeItem } from "metabase/components/tree/types"; import type Database from "metabase-lib/metadata/Database"; @@ -13,8 +11,9 @@ import type { DataPickerSelectedItem } from "../types"; import EmptyState from "../EmptyState"; import LoadingState from "../LoadingState"; +import VirtualizedSelectList from "../VirtualizedSelectList"; import PanePicker from "../PanePicker"; -import { StyledSelectList } from "./RawDataPicker.styled"; +import { ListContainer } from "./RawDataPicker.styled"; interface RawDataPickerViewProps { databases: Database[]; @@ -61,7 +60,7 @@ function TableSelectListItem({ }) { const name = table.displayName(); return ( - <SelectList.Item + <VirtualizedSelectList.Item id={table.id} name={name} isSelected={isSelected} @@ -69,7 +68,7 @@ function TableSelectListItem({ onSelect={onSelect} > {name} - </SelectList.Item> + </VirtualizedSelectList.Item> ); } @@ -135,9 +134,8 @@ function RawDataPickerView({ ); const renderTable = useCallback( - (table: Table) => ( + ({ item: table }: { item: Table }) => ( <TableSelectListItem - key={table.id} table={table} isSelected={selectedTableIds.includes(table.id)} onSelect={onSelectedTable} @@ -162,7 +160,14 @@ function RawDataPickerView({ ) : isEmpty ? ( <EmptyState /> ) : ( - <StyledSelectList>{tables?.map?.(renderTable)}</StyledSelectList> + <ListContainer> + {Array.isArray(tables) && ( + <VirtualizedSelectList<Table> + items={tables} + renderItem={renderTable} + /> + )} + </ListContainer> )} </PanePicker> ); diff --git a/frontend/src/metabase/containers/DataPicker/VirtualizedSelectList.tsx b/frontend/src/metabase/containers/DataPicker/VirtualizedSelectList.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7ad52248e1d46910e97184b24ab18b5678e42aa7 --- /dev/null +++ b/frontend/src/metabase/containers/DataPicker/VirtualizedSelectList.tsx @@ -0,0 +1,28 @@ +import React from "react"; + +import VirtualizedList, { + VirtualizedListProps, +} from "metabase/components/VirtualizedList"; +import SelectList from "metabase/components/SelectList"; + +const SELECT_LIST_ITEM_HEIGHT = 40; + +type VirtualizedSelectListProps<Item> = Omit< + VirtualizedListProps<Item>, + "rowHeight" | "role" +>; + +function VirtualizedSelectList<Item>(props: VirtualizedSelectListProps<Item>) { + return ( + <VirtualizedList<Item> + data-testid="select-list" + {...props} + role="menu" + rowHeight={SELECT_LIST_ITEM_HEIGHT} + /> + ); +} + +export default Object.assign(VirtualizedSelectList, { + Item: SelectList.Item, +}); diff --git a/frontend/src/metabase/containers/DataPicker/tests/DataPicker-Common.unit.spec.ts b/frontend/src/metabase/containers/DataPicker/tests/DataPicker-Common.unit.spec.ts index d2c6dc25d66c30a62be3517668b95730df1a1f16..eb638ade81cee18fa2b40a8c2533e9c0915e5bf4 100644 --- a/frontend/src/metabase/containers/DataPicker/tests/DataPicker-Common.unit.spec.ts +++ b/frontend/src/metabase/containers/DataPicker/tests/DataPicker-Common.unit.spec.ts @@ -1,13 +1,13 @@ import nock from "nock"; -import { screen, waitFor } from "__support__/ui"; +import { screen } from "__support__/ui"; import { SAMPLE_DATABASE } from "__support__/sample_database_fixture"; -import { setup } from "./common"; +import { setup, setupVirtualizedLists } from "./common"; describe("DataPicker", () => { beforeAll(() => { - window.HTMLElement.prototype.scrollIntoView = jest.fn(); + setupVirtualizedLists(); }); afterEach(() => { diff --git a/frontend/src/metabase/containers/DataPicker/tests/DataPicker-Models.unit.spec.ts b/frontend/src/metabase/containers/DataPicker/tests/DataPicker-Models.unit.spec.ts index 44ac210af3e1933291f8542bef5060fc2c6ceac8..a1a7c36d4c195c691fa95a09f5db63df80c08ac0 100644 --- a/frontend/src/metabase/containers/DataPicker/tests/DataPicker-Models.unit.spec.ts +++ b/frontend/src/metabase/containers/DataPicker/tests/DataPicker-Models.unit.spec.ts @@ -13,6 +13,7 @@ import { import { setup, + setupVirtualizedLists, EMPTY_COLLECTION, SAMPLE_COLLECTION, SAMPLE_MODEL, @@ -27,7 +28,7 @@ const ROOT_COLLECTION_MODEL_VIRTUAL_SCHEMA_ID = getCollectionVirtualSchemaId( describe("DataPicker — picking models", () => { beforeAll(() => { - window.HTMLElement.prototype.scrollIntoView = jest.fn(); + setupVirtualizedLists(); }); afterEach(() => { diff --git a/frontend/src/metabase/containers/DataPicker/tests/DataPicker-Questions.unit.spec.ts b/frontend/src/metabase/containers/DataPicker/tests/DataPicker-Questions.unit.spec.ts index 320c707e4a18bac2b4c077bf5096a60968fb72e6..5d38a0b09a837ee4c4749d93ca03921bcb890c52 100644 --- a/frontend/src/metabase/containers/DataPicker/tests/DataPicker-Questions.unit.spec.ts +++ b/frontend/src/metabase/containers/DataPicker/tests/DataPicker-Questions.unit.spec.ts @@ -13,6 +13,7 @@ import { import { setup, + setupVirtualizedLists, EMPTY_COLLECTION, SAMPLE_COLLECTION, SAMPLE_QUESTION, @@ -25,7 +26,7 @@ const ROOT_COLLECTION_QUESTIONS_VIRTUAL_SCHEMA_ID = describe("DataPicker — picking questions", () => { beforeAll(() => { - window.HTMLElement.prototype.scrollIntoView = jest.fn(); + setupVirtualizedLists(); }); afterEach(() => { diff --git a/frontend/src/metabase/containers/DataPicker/tests/DataPicker-RawData.unit.spec.ts b/frontend/src/metabase/containers/DataPicker/tests/DataPicker-RawData.unit.spec.ts index ee9c06872a033d6a7390fcb39a4059ebfaee69f0..db37ba4d10032208040edaf20d60b03211441578 100644 --- a/frontend/src/metabase/containers/DataPicker/tests/DataPicker-RawData.unit.spec.ts +++ b/frontend/src/metabase/containers/DataPicker/tests/DataPicker-RawData.unit.spec.ts @@ -10,11 +10,11 @@ import { import { generateSchemaId } from "metabase-lib/metadata/utils/schema"; -import { setup } from "./common"; +import { setup, setupVirtualizedLists } from "./common"; describe("DataPicker — picking raw data", () => { beforeAll(() => { - window.HTMLElement.prototype.scrollIntoView = jest.fn(); + setupVirtualizedLists(); }); afterEach(() => { diff --git a/frontend/src/metabase/containers/DataPicker/tests/common.tsx b/frontend/src/metabase/containers/DataPicker/tests/common.tsx index 7846c37c9a9deaff706a9f2b9289b5b51cda32c2..3bb1fdb15c47f8feebb5717a1061cfddf0f40657 100644 --- a/frontend/src/metabase/containers/DataPicker/tests/common.tsx +++ b/frontend/src/metabase/containers/DataPicker/tests/common.tsx @@ -111,6 +111,21 @@ interface SetupOpts { hasNestedQueriesEnabled?: boolean; } +// react-virtualized's AutoSizer uses offsetWidth and offsetHeight. +// Jest runs in JSDom which doesn't support measurements APIs. +export function setupVirtualizedLists() { + Object.defineProperty(HTMLElement.prototype, "offsetHeight", { + configurable: true, + value: 100, + }); + Object.defineProperty(HTMLElement.prototype, "offsetWidth", { + configurable: true, + value: 100, + }); + + window.HTMLElement.prototype.scrollIntoView = jest.fn(); +} + export async function setup({ initialValue = { tableIds: [] }, filters,