export function DataTable()

in src/data-table/data-table.tsx [649:1074]


export function DataTable({
  selectable,
  batchActions,
  columns,
  filters,
  emptyMessage,
  loading,
  loadingMessage,
  onIncludedRowsChange,
  onRowHighlightChange,
  onSelectMany,
  onSelectNone,
  onSelectOne,
  onSort,
  resizableColumnWidths = false,
  rows: allRows,
  rowActions = [],
  rowHeight = 36,
  rowHighlightIndex: rowHighlightIndexControlled,
  selectedRowIds,
  sortIndex,
  sortDirection,
  textQuery = '',
  controlRef,
}: DataTableProps) {
  const [, theme] = useStyletron();
  const locale = React.useContext(LocaleContext);

  const rowHeightAtIndex = React.useCallback(
    (index: number) => {
      if (index === 0) {
        return HEADER_ROW_HEIGHT;
      }
      return rowHeight;
    },
    [rowHeight]
  );

  // We use state for our ref, to allow hooks to  update when the ref changes.
  const [gridRef, setGridRef] = React.useState<VariableSizeGrid | undefined | null>(null);
  const [measuredWidths, setMeasuredWidths] = React.useState(columns.map(() => 0));
  const [resizeDeltas, setResizeDeltas] = React.useState(columns.map(() => 0));
  React.useEffect(() => {
    setMeasuredWidths((prev) => {
      return columns.map((v, index) => prev[index] || 0);
    });
    setResizeDeltas((prev) => {
      return columns.map((v, index) => prev[index] || 0);
    });
  }, [columns]);

  const resetAfterColumnIndex = React.useCallback(
    (columnIndex: number) => {
      if (gridRef) {
        // trigger react-window to layout the elements again
        gridRef.resetAfterColumnIndex(columnIndex, true);
      }
    },
    [gridRef]
  );
  const handleWidthsChange = React.useCallback(
    (nextWidths: number[]) => {
      setMeasuredWidths(nextWidths);
      resetAfterColumnIndex(0);
    },
    [setMeasuredWidths, resetAfterColumnIndex]
  );
  const handleColumnResize = React.useCallback(
    (columnIndex: number, delta: number) => {
      setResizeDeltas((prev) => {
        prev[columnIndex] = Math.max(prev[columnIndex] + delta, 0);
        return [...prev];
      });
      resetAfterColumnIndex(columnIndex);
    },
    [setResizeDeltas, resetAfterColumnIndex]
  );

  const [scrollLeft, setScrollLeft] = React.useState(0);
  const [isScrollingX, setIsScrollingX] = React.useState(false);
  const [recentlyScrolledX, setRecentlyScrolledX] = React.useState(false);
  React.useLayoutEffect(() => {
    if (recentlyScrolledX !== isScrollingX) {
      setIsScrollingX(recentlyScrolledX);
    }

    if (recentlyScrolledX) {
      const timeout = setTimeout(() => {
        setRecentlyScrolledX(false);
      }, 200);
      return () => clearTimeout(timeout);
    }
  }, [recentlyScrolledX]);
  const handleScroll = React.useCallback(
    (params: { scrollLeft: number }) => {
      setScrollLeft(params.scrollLeft);
      if (params.scrollLeft !== scrollLeft) {
        setRecentlyScrolledX(true);
      }
    },
    [scrollLeft, setScrollLeft, setRecentlyScrolledX]
  );

  const sortedIndices = React.useMemo(() => {
    let toSort = allRows.map((r, i): [DataTableProps['rows'][number], number] => [r, i]);
    const index = sortIndex;

    if (index !== null && index !== undefined && index !== -1 && columns[index]) {
      const sortFn = columns[index].sortFn;
      // @ts-ignore
      const getValue = (row) => columns[index].mapDataToValue(row.data);
      if (sortDirection === SORT_DIRECTIONS.ASC) {
        toSort.sort((a, b) => sortFn(getValue(a[0]), getValue(b[0])));
      } else if (sortDirection === SORT_DIRECTIONS.DESC) {
        toSort.sort((a, b) => sortFn(getValue(b[0]), getValue(a[0])));
      }
    }

    return toSort.map((el) => el[1]);
  }, [sortIndex, sortDirection, columns, allRows]);

  const filteredIndices = React.useMemo(() => {
    const set = new Set(allRows.map((_, idx) => idx));
    // @ts-ignore
    Array.from(filters || new Set(), (f) => f).forEach(([title, filter]) => {
      const columnIndex = columns.findIndex((c) => c.title === title);
      const column = columns[columnIndex];
      if (!column) {
        return;
      }

      const filterFn = column.buildFilter(filter);
      Array.from(set).forEach((idx) => {
        if (!filterFn(column.mapDataToValue(allRows[idx].data))) {
          set.delete(idx);
        }
      });
    });

    if (textQuery) {
      // @ts-ignore
      const stringishColumnIndices = [];
      for (let i = 0; i < columns.length; i++) {
        if (columns[i].textQueryFilter) {
          // @ts-ignore
          stringishColumnIndices.push(i);
        }
      }
      Array.from(set).forEach((idx) => {
        // @ts-ignore
        const matches = stringishColumnIndices.some((cdx) => {
          const column = columns[cdx];
          const textQueryFilter = column.textQueryFilter;
          if (textQueryFilter) {
            return textQueryFilter(textQuery, column.mapDataToValue(allRows[idx].data));
          }
          return false;
        });

        if (!matches) {
          set.delete(idx);
        }
      });
    }

    return set;
  }, [filters, textQuery, columns, allRows]);

  const rows = React.useMemo(() => {
    const result = sortedIndices
      .filter((idx) => filteredIndices.has(idx))
      .map((idx) => allRows[idx]);

    if (onIncludedRowsChange) {
      onIncludedRowsChange(result);
    }
    return result;
  }, [sortedIndices, filteredIndices, onIncludedRowsChange, allRows]);

  const [browserScrollbarWidth, setBrowserScrollbarWidth] = React.useState(0);
  const normalizedWidths = React.useMemo(() => {
    const resizedWidths = measuredWidths.map((w, i) => Math.floor(w) + Math.floor(resizeDeltas[i]));
    if (gridRef) {
      const gridProps = gridRef.props;

      let isContentTallerThanContainer = false;
      let visibleRowHeight = 0;
      for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
        visibleRowHeight += rowHeightAtIndex(rowIndex);
        if (visibleRowHeight >= gridProps.height) {
          isContentTallerThanContainer = true;
          break;
        }
      }

      const scrollbarWidth = isContentTallerThanContainer ? browserScrollbarWidth : 0;

      const remainder = gridProps.width - sum(resizedWidths) - scrollbarWidth;
      const padding = Math.floor(
        remainder / columns.filter((c) => (c ? c.fillWidth : true)).length
      );
      if (padding > 0) {
        const result = [];
        // -1 so that we loop over all but the last item
        for (let i = 0; i < resizedWidths.length - 1; i++) {
          if (columns[i] && columns[i].fillWidth) {
            // @ts-ignore
            result.push(resizedWidths[i] + padding);
          } else {
            // @ts-ignore
            result.push(resizedWidths[i]);
          }
        }
        // @ts-ignore
        result.push(gridProps.width - sum(result) - scrollbarWidth);
        return result;
      }
    }
    return resizedWidths;
  }, [gridRef, measuredWidths, resizeDeltas, browserScrollbarWidth, rows.length, columns]);

  React.useEffect(() => {
    resetAfterColumnIndex(0);
  }, [normalizedWidths]);

  const isSelectable = (batchActions ? !!batchActions.length : false) || !!selectable;
  const isSelectedAll = React.useMemo(() => {
    if (!selectedRowIds) {
      return false;
    }
    return !!rows.length && selectedRowIds.size >= rows.length;
  }, [selectedRowIds, rows.length]);
  const isSelectedIndeterminate = React.useMemo(() => {
    if (!selectedRowIds) {
      return false;
    }
    return !!selectedRowIds.size && selectedRowIds.size < rows.length;
  }, [selectedRowIds, rows.length]);
  const isRowSelected = React.useCallback(
    (id: string | number) => {
      if (selectedRowIds) {
        return selectedRowIds.has(id);
      }
      return false;
    },
    [selectedRowIds]
  );
  const handleSelectMany = React.useCallback(() => {
    if (onSelectMany) {
      onSelectMany(rows);
    }
  }, [rows, onSelectMany]);
  const handleSelectNone = React.useCallback(() => {
    if (onSelectNone) {
      onSelectNone();
    }
  }, [onSelectNone]);
  const handleSelectOne = React.useCallback(
    (row: Row) => {
      if (onSelectOne) {
        onSelectOne(row);
      }
    },
    [onSelectOne]
  );

  const handleSort = React.useCallback(
    (columnIndex: number) => {
      if (onSort) {
        onSort(columnIndex);
      }
    },
    [onSort]
  );

  React.useImperativeHandle(
    controlRef,
    () => ({
      clearSelection: handleSelectNone,
      getRows: () => rows,
    }),
    [handleSelectNone, rows]
  );

  const [columnHighlightIndex, setColumnHighlightIndex] = React.useState(-1);
  const [rowHighlightIndex, setRowHighlightIndex] = React.useState(-1);

  // @ts-ignore
  function handleRowHighlightIndexChange(nextIndex) {
    setRowHighlightIndex(nextIndex);
    if (gridRef) {
      if (nextIndex >= 0) {
        gridRef.scrollToItem({ rowIndex: nextIndex });
      }
      if (onRowHighlightChange) {
        onRowHighlightChange(nextIndex, rows[nextIndex - 1]);
      }
    }
  }

  const handleRowMouseEnter = React.useCallback(
    (nextIndex: number) => {
      setColumnHighlightIndex(-1);
      if (nextIndex !== rowHighlightIndex) {
        handleRowHighlightIndexChange(nextIndex);
      }
    },
    [rowHighlightIndex]
  );
  // @ts-ignore
  function handleColumnHeaderMouseEnter(columnIndex) {
    setColumnHighlightIndex(columnIndex);
    handleRowHighlightIndexChange(-1);
  }
  function handleColumnHeaderMouseLeave() {
    setColumnHighlightIndex(-1);
  }

  React.useEffect(() => {
    if (typeof rowHighlightIndexControlled === 'number') {
      handleRowHighlightIndexChange(rowHighlightIndexControlled);
    }
  }, [rowHighlightIndexControlled]);

  const itemData = React.useMemo(() => {
    return {
      columnHighlightIndex,
      rowHighlightIndex,
      isRowSelected,
      isSelectable,
      onRowMouseEnter: handleRowMouseEnter,
      onSelectOne: handleSelectOne,
      columns: columns,
      rows,
      textQuery,
    };
  }, [
    handleRowMouseEnter,
    columnHighlightIndex,
    isRowSelected,
    isSelectable,
    rowHighlightIndex,
    rows,
    columns,
    handleSelectOne,
    textQuery,
  ]);

  return (
    <React.Fragment>
      <MeasureColumnWidths
        columns={columns}
        rows={rows}
        widths={measuredWidths}
        isSelectable={isSelectable}
        onWidthsChange={handleWidthsChange}
      />
      {/* @ts-ignore */}
      <MeasureScrollbarWidth
        // @ts-ignore
        onWidthChange={(w) => setBrowserScrollbarWidth(w)}
      />
      <AutoSizer>
        {/* @ts-ignore */}
        {({ height, width }) => (
          <HeaderContext.Provider
            value={{
              columns: columns,
              columnHighlightIndex,
              emptyMessage: emptyMessage || locale.datatable.emptyState,
              filters: filters,
              loading: Boolean(loading),
              loadingMessage: loadingMessage || locale.datatable.loadingState,
              isScrollingX,
              isSelectable,
              isSelectedAll,
              isSelectedIndeterminate,
              measuredWidths,
              onMouseEnter: handleColumnHeaderMouseEnter,
              onMouseLeave: handleColumnHeaderMouseLeave,
              onResize: handleColumnResize,
              onSelectMany: handleSelectMany,
              onSelectNone: handleSelectNone,
              onSort: handleSort,
              resizableColumnWidths,
              rowActions,
              rowHeight,
              rowHighlightIndex,
              rows,
              scrollLeft,
              // @ts-ignore
              sortDirection: sortDirection || null,
              sortIndex: typeof sortIndex === 'number' ? sortIndex : -1,
              tableHeight: height,
              widths: normalizedWidths,
            }}
          >
            <VariableSizeGrid
              // eslint-disable-next-line @typescript-eslint/no-explicit-any
              ref={(node) => setGridRef(node)}
              overscanRowCount={10}
              overscanColumnCount={5}
              innerElementType={InnerTableElement}
              columnCount={columns.length}
              columnWidth={(columnIndex) => normalizedWidths[columnIndex]}
              height={height - 2}
              // plus one to account for additional header row
              rowCount={rows.length + 1}
              rowHeight={rowHeightAtIndex}
              width={width - 2}
              itemData={itemData}
              onScroll={handleScroll}
              style={{
                ...theme.borders.border200,
                borderColor: theme.colors.borderOpaque,
              }}
              direction={theme.direction === 'rtl' ? 'rtl' : 'ltr'}
            >
              {CellPlacementMemo}
            </VariableSizeGrid>
          </HeaderContext.Provider>
        )}
      </AutoSizer>
    </React.Fragment>
  );
}