diff --git a/packages/odyssey-react-mui/src/DataGrid.tsx b/packages/odyssey-react-mui/src/DataGrid.tsx index a8dbc0aecf..24e3d6b33f 100644 --- a/packages/odyssey-react-mui/src/DataGrid.tsx +++ b/packages/odyssey-react-mui/src/DataGrid.tsx @@ -10,10 +10,28 @@ * See the License for the specific language governing permissions and limitations under the License. */ -import MaterialReactTable from "material-react-table"; -import { memo } from "react"; +import { Typography } from "@mui/material"; +import MaterialReactTable, { + MRT_ColumnFiltersState, + MRT_FilterOptionMenu, + MRT_FullScreenToggleButton, + MRT_RowSelectionState, + MRT_ShowHideColumnsButton, + MRT_TableInstance, + MRT_Virtualizer, +} from "material-react-table"; +import { + FunctionComponent, + memo, + UIEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; -export type { MRT_ColumnDef as DataGridColumnType } from "material-react-table"; +export type { MRT_ColumnDef as DataGridColumn } from "material-react-table"; type DefaultMaterialReactTableData = Record; @@ -23,28 +41,159 @@ type MaterialReactTableProps = export type DataGridProps = { columns: MaterialReactTableProps["columns"]; data: MaterialReactTableProps["data"]; + fetchMoreData?: () => void; getRowId?: MaterialReactTableProps["getRowId"]; + hasError?: boolean; hasRowSelection?: boolean; + initialState?: MaterialReactTableProps["initialState"]; + isFetching?: boolean; + onGlobalFilterChange?: MaterialReactTableProps["onGlobalFilterChange"]; + onPaginationChange?: MaterialReactTableProps["onPaginationChange"]; onRowSelectionChange?: MaterialReactTableProps["onRowSelectionChange"]; - tableState?: MaterialReactTableProps["state"]; + // rowsPerPageOptions?: MaterialReactTableProps["muiTablePaginationProps"]['rowsPerPageOptions']; + state?: MaterialReactTableProps["state"]; + ToolbarButtons?: FunctionComponent< + { table: MRT_TableInstance } & unknown + >; }; +// Once the user has scrolled within this many pixels of the bottom of the table, fetch more data if we can. +const scrollAmountBeforeFetchingData = 400; + const DataGrid = ({ columns, data, + fetchMoreData, getRowId, + hasError, hasRowSelection, - onRowSelectionChange, - tableState, + initialState, + isFetching, + onGlobalFilterChange, + onPaginationChange, + onRowSelectionChange: onRowSelectionChangeProp, + state, + ToolbarButtons, }: DataGridProps) => { + const tableContainerRef = useRef(null); + + const rowVirtualizerInstanceRef = + useRef>(null); + + const [columnFilters, setColumnFilters] = useState( + [] + ); + + const [globalFilter, setGlobalFilter] = useState(); + + useEffect(() => { + if (globalFilter) { + onGlobalFilterChange?.(globalFilter); + } + }, [globalFilter, onGlobalFilterChange]); + + const totalFetchedRows = data.length ?? 0; + + const fetchMoreOnBottomReached = useCallback( + (containerRefElement?: HTMLDivElement | null) => { + if (containerRefElement) { + const { scrollHeight, scrollTop, clientHeight } = containerRefElement; + + if ( + scrollHeight - scrollTop - clientHeight < + scrollAmountBeforeFetchingData && + !isFetching + ) { + fetchMoreData?.(); + } + } + }, + [fetchMoreData, isFetching] + ); + + useEffect(() => { + try { + // Scroll to top of table when sorting or filters change. + rowVirtualizerInstanceRef.current?.scrollToIndex?.(0); + } catch (error) { + console.error(error); + } + }, [columnFilters, globalFilter]); + + // Check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data. + useEffect(() => { + fetchMoreOnBottomReached(tableContainerRef.current); + }, [fetchMoreOnBottomReached]); + + const renderBottomToolbarCustomActions = useCallback( + () => + fetchMoreData ? ( + Fetched {totalFetchedRows} total rows + ) : ( + {totalFetchedRows} rows + ), + [fetchMoreData, totalFetchedRows] + ); + + // table: MRT_TableInstance + const renderTopToolbarCustomActions = useCallback< + Exclude< + MaterialReactTableProps["renderTopToolbarCustomActions"], + undefined + > + >( + ({ table }) => <>{ToolbarButtons && }, + [ToolbarButtons] + ); + + const [rowSelection, setRowSelection] = useState({}); + + useEffect(() => { + onRowSelectionChangeProp?.(rowSelection); + }, [onRowSelectionChangeProp, rowSelection]); + + const modifiedState = useMemo( + () => ({ + ...state, + rowSelection, + }), + [rowSelection, state] + ); + return ( 50} + enableSorting={false} getRowId={getRowId} - onRowSelectionChange={onRowSelectionChange} - state={tableState} + initialState={initialState} + muiTableContainerProps={{ + onScroll: (event: UIEvent) => + fetchMoreOnBottomReached(event.target as HTMLDivElement), + ref: tableContainerRef, + sx: { maxHeight: String(500 / 14).concat("rem") }, + }} + muiToolbarAlertBannerProps={ + hasError + ? { + children: "Error loading data.", + color: "error", + } + : undefined + } + onColumnFiltersChange={setColumnFilters} + onGlobalFilterChange={setGlobalFilter} + onPaginationChange={onPaginationChange} + onRowSelectionChange={setRowSelection} + renderBottomToolbarCustomActions={renderBottomToolbarCustomActions} + renderTopToolbarCustomActions={renderTopToolbarCustomActions} + rowVirtualizerInstanceRef={rowVirtualizerInstanceRef} + rowVirtualizerProps={{ overscan: 4 }} + state={modifiedState} /> ); }; diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/DataGrid/DataGrid.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/DataGrid/DataGrid.stories.tsx index defdae30e5..1867f8ef7f 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/DataGrid/DataGrid.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/DataGrid/DataGrid.stories.tsx @@ -11,9 +11,16 @@ */ import { Meta, StoryObj } from "@storybook/react"; -import { DataGrid, DataGridColumnType } from "@okta/odyssey-react-mui"; +import { + Button, + DataGrid, + DataGridColumn, + DataGridProps, + Link, +} from "@okta/odyssey-react-mui"; import { MuiThemeDecorator } from "../../../../.storybook/components"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; const storybookMeta: Meta = { title: "MUI Components/Data Grid", @@ -28,9 +35,33 @@ const storybookMeta: Meta = { getRowId: { control: "function", }, + fetchMoreData: { + control: "function", + }, + hasError: { + control: "boolean", + }, hasRowSelection: { control: "boolean", }, + initialState: { + control: "object", + }, + isFetching: { + control: "boolean", + }, + onGlobalFilterChange: { + control: "function", + }, + onPaginationChange: { + control: "function", + }, + onRowSelectionChange: { + control: "function", + }, + state: { + control: "object", + }, }, decorators: [MuiThemeDecorator], }; @@ -48,7 +79,7 @@ type Person = { state: string; }; -const columns: DataGridColumnType[] = [ +const columns: DataGridColumn[] = [ { accessorKey: "name.firstName", header: "First Name", @@ -123,9 +154,387 @@ const data: Person[] = [ }, state: "Nebraska", }, + { + address: "261 Erdman Ford", + city: "East Daphne", + id: "334823773", + name: { + firstName: "John", + lastName: "Doe", + }, + state: "Kentucky", + }, + { + address: "769 Dominic Grove", + city: "Columbus", + id: "334823774", + name: { + firstName: "Jane", + lastName: "Doe", + }, + state: "Ohio", + }, + { + address: "566 Brakus Inlet", + city: "South Linda", + id: "334823775", + name: { + firstName: "Joe", + lastName: "Doe", + }, + state: "West Virginia", + }, + { + address: "722 Emie Stream", + city: "Lincoln", + id: "334823776", + name: { + firstName: "Kevin", + lastName: "Vandy", + }, + state: "Nebraska", + }, + { + address: "32188 Larkin Turnpike", + city: "Omaha", + id: "334823777", + name: { + firstName: "Joshua", + lastName: "Rolluffs", + }, + state: "Nebraska", + }, + { + address: "261 Erdman Ford", + city: "East Daphne", + id: "434823773", + name: { + firstName: "John", + lastName: "Doe", + }, + state: "Kentucky", + }, + { + address: "769 Dominic Grove", + city: "Columbus", + id: "434823774", + name: { + firstName: "Jane", + lastName: "Doe", + }, + state: "Ohio", + }, + { + address: "566 Brakus Inlet", + city: "South Linda", + id: "434823775", + name: { + firstName: "Joe", + lastName: "Doe", + }, + state: "West Virginia", + }, + { + address: "722 Emie Stream", + city: "Lincoln", + id: "434823776", + name: { + firstName: "Kevin", + lastName: "Vandy", + }, + state: "Nebraska", + }, + { + address: "32188 Larkin Turnpike", + city: "Omaha", + id: "434823777", + name: { + firstName: "Joshua", + lastName: "Rolluffs", + }, + state: "Nebraska", + }, + { + address: "261 Erdman Ford", + city: "East Daphne", + id: "534823773", + name: { + firstName: "John", + lastName: "Doe", + }, + state: "Kentucky", + }, + { + address: "769 Dominic Grove", + city: "Columbus", + id: "534823774", + name: { + firstName: "Jane", + lastName: "Doe", + }, + state: "Ohio", + }, + { + address: "566 Brakus Inlet", + city: "South Linda", + id: "534823775", + name: { + firstName: "Joe", + lastName: "Doe", + }, + state: "West Virginia", + }, + { + address: "722 Emie Stream", + city: "Lincoln", + id: "534823776", + name: { + firstName: "Kevin", + lastName: "Vandy", + }, + state: "Nebraska", + }, + { + address: "32188 Larkin Turnpike", + city: "Omaha", + id: "534823777", + name: { + firstName: "Joshua", + lastName: "Rolluffs", + }, + state: "Nebraska", + }, + { + address: "261 Erdman Ford", + city: "East Daphne", + id: "634823773", + name: { + firstName: "John", + lastName: "Doe", + }, + state: "Kentucky", + }, + { + address: "769 Dominic Grove", + city: "Columbus", + id: "634823774", + name: { + firstName: "Jane", + lastName: "Doe", + }, + state: "Ohio", + }, + { + address: "566 Brakus Inlet", + city: "South Linda", + id: "634823775", + name: { + firstName: "Joe", + lastName: "Doe", + }, + state: "West Virginia", + }, + { + address: "722 Emie Stream", + city: "Lincoln", + id: "634823776", + name: { + firstName: "Kevin", + lastName: "Vandy", + }, + state: "Nebraska", + }, + { + address: "32188 Larkin Turnpike", + city: "Omaha", + id: "634823777", + name: { + firstName: "Joshua", + lastName: "Rolluffs", + }, + state: "Nebraska", + }, + { + address: "261 Erdman Ford", + city: "East Daphne", + id: "734823773", + name: { + firstName: "John", + lastName: "Doe", + }, + state: "Kentucky", + }, + { + address: "769 Dominic Grove", + city: "Columbus", + id: "734823774", + name: { + firstName: "Jane", + lastName: "Doe", + }, + state: "Ohio", + }, + { + address: "566 Brakus Inlet", + city: "South Linda", + id: "734823775", + name: { + firstName: "Joe", + lastName: "Doe", + }, + state: "West Virginia", + }, + { + address: "722 Emie Stream", + city: "Lincoln", + id: "734823776", + name: { + firstName: "Kevin", + lastName: "Vandy", + }, + state: "Nebraska", + }, + { + address: "32188 Larkin Turnpike", + city: "Omaha", + id: "734823777", + name: { + firstName: "Joshua", + lastName: "Rolluffs", + }, + state: "Nebraska", + }, + { + address: "261 Erdman Ford", + city: "East Daphne", + id: "834823773", + name: { + firstName: "John", + lastName: "Doe", + }, + state: "Kentucky", + }, + { + address: "769 Dominic Grove", + city: "Columbus", + id: "834823774", + name: { + firstName: "Jane", + lastName: "Doe", + }, + state: "Ohio", + }, + { + address: "566 Brakus Inlet", + city: "South Linda", + id: "834823775", + name: { + firstName: "Joe", + lastName: "Doe", + }, + state: "West Virginia", + }, + { + address: "722 Emie Stream", + city: "Lincoln", + id: "834823776", + name: { + firstName: "Kevin", + lastName: "Vandy", + }, + state: "Nebraska", + }, + { + address: "32188 Larkin Turnpike", + city: "Omaha", + id: "834823777", + name: { + firstName: "Joshua", + lastName: "Rolluffs", + }, + state: "Nebraska", + }, + { + address: "261 Erdman Ford", + city: "East Daphne", + id: "934823773", + name: { + firstName: "John", + lastName: "Doe", + }, + state: "Kentucky", + }, + { + address: "769 Dominic Grove", + city: "Columbus", + id: "934823774", + name: { + firstName: "Jane", + lastName: "Doe", + }, + state: "Ohio", + }, + { + address: "566 Brakus Inlet", + city: "South Linda", + id: "934823775", + name: { + firstName: "Joe", + lastName: "Doe", + }, + state: "West Virginia", + }, + { + address: "722 Emie Stream", + city: "Lincoln", + id: "934823776", + name: { + firstName: "Kevin", + lastName: "Vandy", + }, + state: "Nebraska", + }, + { + address: "32188 Larkin Turnpike", + city: "Omaha", + id: "934823777", + name: { + firstName: "Joshua", + lastName: "Rolluffs", + }, + state: "Nebraska", + }, ]; -export const Example: StoryObj = { +export const Basic: StoryObj> = { + args: { + columns, + data, + getRowId: ({ id }: { id: string }) => id, + }, +}; + +export const InfiniteScroll: StoryObj> = { + args: { + columns, + data, + getRowId: ({ id }: { id: string }) => id, + }, + render: function C(args) { + const countRef = useRef(15); + const [data, setData] = useState(args.data.slice(0, countRef.current)); + + const fetchMoreData = useCallback(() => { + countRef.current = countRef.current + 5; + + setData(args.data.slice(0, Math.min(countRef.current, args.data.length))); + }, [args.data]); + + return ; + }, +}; + +export const Selection: StoryObj> = { args: { columns, data, @@ -133,3 +542,147 @@ export const Example: StoryObj = { hasRowSelection: true, }, }; + +export const CustomToolbar: StoryObj> = { + args: { + columns, + data, + getRowId: ({ id }: { id: string }) => id, + ToolbarButtons: ({ table }) => ( +