Use DataTable as list base

Summary:
Changelog: Standardized DataList component

This diff standardizes the DataList component, by reusing the DataList. This is done to be able to take full advantage of all its features like virtualisation, keyboard support, datasource support, etc.

Also cleaned up DataTable properties a bit, by prefixing all flags with `enableXXX` and setting clear defaults

Reviewed By: passy

Differential Revision: D28119721

fbshipit-source-id: b7b241ea18d788bfa035389cc8c6ae7ea95ecadb
This commit is contained in:
Michel Weststrate
2021-05-04 13:49:11 -07:00
committed by Facebook GitHub Bot
parent 5bf9541e05
commit d903a862d2
11 changed files with 244 additions and 157 deletions

View File

@@ -51,30 +51,45 @@ import {useInUnitTest} from '../../utils/useInUnitTest()';
interface DataTableBaseProps<T = any> {
columns: DataTableColumn<T>[];
autoScroll?: boolean;
enableSearchbar?: boolean;
enableAutoScroll?: boolean;
enableColumnHeaders?: boolean;
enableMultiSelect?: boolean;
// if set (the default) will grow and become scrollable. Otherwise will use natural size
scrollable?: boolean;
extraActions?: React.ReactElement;
onSelect?(record: T | undefined, records: T[]): void;
onRowStyle?(record: T): CSSProperties | undefined;
// multiselect?: true
tableManagerRef?: RefObject<DataTableManager<T> | undefined>; // Actually we want a MutableRefObject, but that is not what React.createRef() returns, and we don't want to put the burden on the plugin dev to cast it...
onCopyRows?(records: T[]): string;
onContextMenu?: (selection: undefined | T) => React.ReactElement;
searchbar?: boolean;
scrollable?: boolean;
onRenderEmpty?:
| null
| ((dataSource?: DataSource<T, any, any>) => React.ReactElement);
}
export type ItemRenderer<T> = (
item: T,
selected: boolean,
index: number,
) => React.ReactNode;
type DataTableInput<T = any> =
| {dataSource: DataSource<T, any, any>; records?: undefined}
| {
records: T[];
dataSource: DataSource<T, any, any>;
records?: undefined;
recordsKey?: undefined;
}
| {
records: readonly T[];
recordsKey?: keyof T;
dataSource?: undefined;
};
export type DataTableColumn<T = any> = {
key: keyof T & string;
// possible future extension: getValue(row) (and free-form key) to support computed columns
onRender?: (row: T) => React.ReactNode;
onRender?: (row: T, selected: boolean, index: number) => React.ReactNode;
formatters?: Formatter[] | Formatter;
title?: string;
width?: number | Percentage | undefined; // undefined: use all remaining width
@@ -89,7 +104,7 @@ export type DataTableColumn<T = any> = {
}[];
};
export interface RenderContext<T = any> {
export interface TableRowRenderContext<T = any> {
columns: DataTableColumn<T>[];
onMouseEnter(
e: React.MouseEvent<HTMLDivElement>,
@@ -101,6 +116,7 @@ export interface RenderContext<T = any> {
item: T,
itemId: number,
): void;
onRowStyle?(item: T): React.CSSProperties | undefined;
}
export type DataTableProps<T> = DataTableInput<T> & DataTableBaseProps<T>;
@@ -116,6 +132,7 @@ export function DataTable<T extends object>(
useAssertStableRef(props.columns, 'columns');
useAssertStableRef(onCopyRows, 'onCopyRows');
useAssertStableRef(onContextMenu, 'onContextMenu');
const isUnitTest = useInUnitTest();
// eslint-disable-next-line
@@ -131,7 +148,7 @@ export function DataTable<T extends object>(
onSelect,
scope,
virtualizerRef,
autoScroll: props.autoScroll,
autoScroll: props.enableAutoScroll,
}),
);
@@ -154,7 +171,7 @@ export function DataTable<T extends object>(
[columns],
);
const renderingConfig = useMemo<RenderContext<T>>(() => {
const renderingConfig = useMemo<TableRowRenderContext<T>>(() => {
let startIndex = 0;
return {
columns: visibleColumns,
@@ -185,14 +202,15 @@ export function DataTable<T extends object>(
document.addEventListener('mouseup', onStopDragSelecting);
}
},
onRowStyle,
};
}, [visibleColumns, tableManager]);
}, [visibleColumns, tableManager, onRowStyle]);
const itemRenderer = useCallback(
function itemRenderer(
record: T,
index: number,
renderContext: RenderContext<T>,
renderContext: TableRowRenderContext<T>,
) {
return (
<TableRow
@@ -357,11 +375,11 @@ export function DataTable<T extends object>(
const onUpdateAutoScroll = useCallback(
(autoScroll: boolean) => {
if (props.autoScroll) {
if (props.enableAutoScroll) {
dispatch({type: 'setAutoScroll', autoScroll});
}
},
[props.autoScroll],
[props.enableAutoScroll],
);
/** Context menu */
@@ -409,7 +427,7 @@ export function DataTable<T extends object>(
const header = (
<Layout.Container>
{props.searchbar !== false && (
{props.enableSearchbar && (
<TableSearch
searchValue={searchValue}
useRegex={tableState.useRegex}
@@ -418,56 +436,61 @@ export function DataTable<T extends object>(
extraActions={props.extraActions}
/>
)}
<TableHead
visibleColumns={visibleColumns}
dispatch={dispatch as any}
sorting={sorting}
scrollbarSize={
props.scrollable === false
? 0
: 15 /* width on MacOS: TODO, determine dynamically */
}
{props.enableColumnHeaders && (
<TableHead
visibleColumns={visibleColumns}
dispatch={dispatch as any}
sorting={sorting}
scrollbarSize={
props.scrollable
? 0
: 15 /* width on MacOS: TODO, determine dynamically */
}
/>
)}
</Layout.Container>
);
const emptyRenderer =
props.onRenderEmpty === undefined
? props.onRenderEmpty
: props.onRenderEmpty;
const mainSection = props.scrollable ? (
<Layout.Top>
{header}
<DataSourceRenderer<T, TableRowRenderContext<T>>
dataSource={dataSource}
autoScroll={tableState.autoScroll && !dragging.current}
useFixedRowHeight={!tableState.usesWrapping}
defaultRowHeight={DEFAULT_ROW_HEIGHT}
context={renderingConfig}
itemRenderer={itemRenderer}
onKeyDown={onKeyDown}
virtualizerRef={virtualizerRef}
onRangeChange={onRangeChange}
onUpdateAutoScroll={onUpdateAutoScroll}
emptyRenderer={emptyRenderer}
/>
</Layout.Top>
) : (
<Layout.Container>
{header}
<StaticDataSourceRenderer<T, TableRowRenderContext<T>>
dataSource={dataSource}
useFixedRowHeight={!tableState.usesWrapping}
defaultRowHeight={DEFAULT_ROW_HEIGHT}
context={renderingConfig}
itemRenderer={itemRenderer}
onKeyDown={onKeyDown}
emptyRenderer={emptyRenderer}
/>
</Layout.Container>
);
const mainSection =
props.scrollable !== false ? (
<Layout.Top>
{header}
<DataSourceRenderer<T, RenderContext<T>>
dataSource={dataSource}
autoScroll={tableState.autoScroll && !dragging.current}
useFixedRowHeight={!tableState.usesWrapping}
defaultRowHeight={DEFAULT_ROW_HEIGHT}
context={renderingConfig}
itemRenderer={itemRenderer}
onKeyDown={onKeyDown}
virtualizerRef={virtualizerRef}
onRangeChange={onRangeChange}
onUpdateAutoScroll={onUpdateAutoScroll}
emptyRenderer={emptyRenderer}
/>
</Layout.Top>
) : (
<Layout.Container>
{header}
<StaticDataSourceRenderer<T, RenderContext<T>>
dataSource={dataSource}
useFixedRowHeight={!tableState.usesWrapping}
defaultRowHeight={DEFAULT_ROW_HEIGHT}
context={renderingConfig}
itemRenderer={itemRenderer}
onKeyDown={onKeyDown}
emptyRenderer={emptyRenderer}
/>
</Layout.Container>
);
return (
<Layout.Container grow={props.scrollable !== false}>
<Layout.Container grow={props.scrollable}>
{mainSection}
{props.autoScroll && (
{props.enableAutoScroll && (
<AutoScroller>
<PushpinFilled
style={{
@@ -484,6 +507,15 @@ export function DataTable<T extends object>(
);
}
DataTable.defaultProps = {
scrollable: true,
enableSearchbar: true,
enableAutoScroll: false,
enableColumnHeaders: true,
eanbleMultiSelect: true,
onRenderEmpty: emptyRenderer,
} as Partial<DataTableProps<any>>;
/* eslint-disable react-hooks/rules-of-hooks */
function normalizeDataSourceInput<T>(props: DataTableInput<T>): DataSource<T> {
if (props.dataSource) {
@@ -491,7 +523,7 @@ function normalizeDataSourceInput<T>(props: DataTableInput<T>): DataSource<T> {
}
if (props.records) {
const [dataSource] = useState(() => {
const ds = new DataSource<T>(undefined);
const ds = new DataSource<T>(props.recordsKey);
syncRecordsToDataSource(ds, props.records);
return ds;
});
@@ -507,7 +539,7 @@ function normalizeDataSourceInput<T>(props: DataTableInput<T>): DataSource<T> {
}
/* eslint-enable */
function syncRecordsToDataSource<T>(ds: DataSource<T>, records: T[]) {
function syncRecordsToDataSource<T>(ds: DataSource<T>, records: readonly T[]) {
const startTime = Date.now();
ds.clear();
// TODO: optimize in the case we're only dealing with appends or replacements